# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: sales_billing.spec.js >> Sales Billing — Cross-stage Integrity >> 360DLM company in invoice resolved by getCompany cache (not hardcoded)
- Location: tests/sales_billing.spec.js:502:3

# Error details

```
Error: expect(received).toContain(expected) // indexOf

Expected substring: "function getCompany("
Received string:    "404 page not found
"
```

# Test source

```ts
  408 |   // job creation now goes exclusively through sales.create_job (ADR-133). Re-fixture or
  409 |   // rewrite against reseeded Acme jobs (JOB-20/JOB-21) if this coverage is needed again.
  410 |   test.skip('job_material_cost rolls up issues per job', async () => {
  411 |     const rows = await dbGet('/job_material_cost?job_id=in.(bt-job-b,bt-job-c)&order=job_id');
  412 |     expect(rows.length).toBe(2);
  413 |     const byId = Object.fromEntries(rows.map(r => [r.job_id, r]));
  414 |     // bt-job-b: 500 flex @70 + 80 standee @250 = 35000 + 20000 = 55000
  415 |     expect(Number(byId['bt-job-b'].material_cost)).toBeCloseTo(55000, 0);
  416 |     expect(Number(byId['bt-job-b'].issue_count)).toBe(2);
  417 |     // bt-job-c: 200 flex @70 + 100 vinyl @100 + 20 standee @250 = 14k+10k+5k = 29000
  418 |     expect(Number(byId['bt-job-c'].material_cost)).toBeCloseTo(29000, 0);
  419 |     expect(Number(byId['bt-job-c'].issue_count)).toBe(3);
  420 |   });
  421 | 
  422 |   // SKIPPED (C7/ADR-133 2026-08-06): dev clean-slate deleted bt-job-b/c P&L fixtures;
  423 |   // job creation now goes exclusively through sales.create_job (ADR-133). Re-fixture or
  424 |   // rewrite against reseeded Acme jobs (JOB-20/JOB-21) if this coverage is needed again.
  425 |   test.skip('job_profitability_v shows positive margin for paid invoice', async () => {
  426 |     const rows = await dbGet('/job_profitability_v?job_id=eq.bt-job-b');
  427 |     const p = rows[0];
  428 |     expect(Number(p.invoiced_amount)).toBeCloseTo(1002056, 0);
  429 |     expect(Number(p.collected_amount)).toBeCloseTo(1002056, 0);
  430 |     expect(Number(p.material_cost)).toBeCloseTo(55000, 0);
  431 |     expect(Number(p.gross_margin)).toBeCloseTo(947056, 0);
  432 |   });
  433 | 
  434 |   // SKIPPED (C7/ADR-133 2026-08-06): dev clean-slate deleted bt-job-b/c P&L fixtures;
  435 |   // job creation now goes exclusively through sales.create_job (ADR-133). Re-fixture or
  436 |   // rewrite against reseeded Acme jobs (JOB-20/JOB-21) if this coverage is needed again.
  437 |   test.skip('job_profitability_v shows negative margin when only costs (no invoice)', async () => {
  438 |     // bt-job-c: draft invoice doesn't count as invoiced; only material cost shown
  439 |     const rows = await dbGet('/job_profitability_v?job_id=eq.bt-job-c');
  440 |     const p = rows[0];
  441 |     expect(Number(p.invoiced_amount)).toBe(0);
  442 |     expect(Number(p.material_cost)).toBeCloseTo(29000, 0);
  443 |     expect(Number(p.gross_margin)).toBeCloseTo(-29000, 0);
  444 |   });
  445 | 
  446 |   test('item_avg_cost computes weighted average from inward_lines', async () => {
  447 |     // Items have a single inward batch each in the test seed, so WAC = unit_price
  448 |     const rows = await dbGet('/item_avg_cost?inward_batches=gte.1&limit=5', 'stores');
  449 |     expect(rows.length).toBeGreaterThan(0);
  450 |     rows.forEach(r => {
  451 |       expect(Number(r.avg_unit_cost)).toBeGreaterThan(0);
  452 |     });
  453 |   });
  454 | 
  455 |   test('Sales PWA has job profitability + materials JS functions', async () => {
  456 |     const ctx = await request.newContext();
  457 |     const html = await (await ctx.get(BASE_URL)).text();
  458 |     expect(html).toContain('loadJobProfitability');
  459 |     expect(html).toContain('loadJobMaterials');
  460 |     expect(html).toContain('job-profit-card');
  461 |     expect(html).toContain('job-materials-card');
  462 |     await ctx.dispose();
  463 |   });
  464 | 
  465 |   test('Custodian PWA has vendor bill link fields', async () => {
  466 |     const ctx = await request.newContext();
  467 |     const html = await (await ctx.get('https://srv1111289.hstgr.cloud/finance/custodian/')).text();
  468 |     expect(html).toContain('id="tf-vbill-link-group"');
  469 |     expect(html).toContain('id="tf-vbill-link"');
  470 |     expect(html).toContain('loadPartyBills');
  471 |     expect(html).toMatch(/custodian-v\d+/);
  472 |     await ctx.dispose();
  473 |   });
  474 | });
  475 | 
  476 | test.describe(`${PWA_NAME} — Cross-stage Integrity`, () => {
  477 |   test('every challan with an invoice_id links to a real invoice', async () => {
  478 |     const challans = await dbGet('/challans?challan_id=like.bt-dc-*&select=challan_id,invoice_id');
  479 |     const linked   = challans.filter(c => c.invoice_id);
  480 |     for (const dc of linked) {
  481 |       const inv = await dbGet(`/invoices?invoice_id=eq.${dc.invoice_id}&select=invoice_id`);
  482 |       expect(inv.length).toBe(1);
  483 |     }
  484 |   });
  485 | 
  486 |   test('every invoice with job_id links to a real job', async () => {
  487 |     const invs = await dbGet('/invoices?invoice_id=like.bt-inv-*&select=invoice_id,job_id');
  488 |     for (const inv of invs) {
  489 |       const job = await dbGet(`/jobs?job_id=eq.${inv.job_id}&select=job_id`);
  490 |       expect(job.length).toBe(1);
  491 |     }
  492 |   });
  493 | 
  494 |   test('invoice_receipts sum ≥ grand_total → invoice is paid', async () => {
  495 |     const { json: summary } = await api('GET', '/invoice_payment_summary?invoice_id=eq.bt-inv-3');
  496 |     const s = summary[0];
  497 |     // bt-inv-3 total = 1002056, receipts = 500000 + 502056 = 1002056
  498 |     expect(Number(s.paid_amount)).toBeGreaterThanOrEqual(Number(s.grand_total) - 1);
  499 |     expect(s.status).toBe('paid');
  500 |   });
  501 | 
  502 |   test('360DLM company in invoice resolved by getCompany cache (not hardcoded)', async () => {
  503 |     const ctx = await request.newContext();
  504 |     const html = await (await ctx.get(BASE_URL)).text();
  505 |     // Must NOT have old hardcoded const COMPANIES
  506 |     expect(html).not.toContain('const COMPANIES =');
  507 |     // Must have getCompany function
> 508 |     expect(html).toContain('function getCompany(');
      |                  ^ Error: expect(received).toContain(expected) // indexOf
  509 |     expect(html).toContain('_companiesCache');
  510 |     await ctx.dispose();
  511 |   });
  512 | 
  513 |   test('gst_type matches company vs client state codes', async () => {
  514 |     // bt-inv-1: PMS (state 03) → client state 07 → IGST ✓
  515 |     // bt-inv-3: PMS (state 03) → client state 27 → IGST ✓
  516 |     const rows = await dbGet('/invoices?invoice_id=in.(bt-inv-1,bt-inv-3)&select=invoice_id,company,client_state,gst_type');
  517 |     rows.forEach(r => {
  518 |       expect(r.gst_type).toBe('IGST'); // both clients are interstate from Punjab PMS
  519 |     });
  520 |   });
  521 | });
  522 | 
  523 | // ──────────────────────────────────────────────────────────────
  524 | // UI: RENDERED SCREENS
  525 | // ──────────────────────────────────────────────────────────────
  526 | test.describe(`${PWA_NAME} — Rendered UI`, () => {
  527 |   test('Dashboard loads and shows Billed ₹ KPI', async ({ page }) => {
  528 |     await injectSession(page);
  529 |     await page.goto(BASE_URL);
  530 |     await page.waitForTimeout(800);
  531 |     await expect(page.locator('#kpi-billed')).toBeVisible();
  532 |     const billed = await page.locator('#kpi-billed').textContent();
  533 |     expect(billed).not.toBe('—');
  534 |   });
  535 | 
  536 |   // SKIPPED (C7/ADR-133 2026-08-06): dev clean-slate deleted bt-job-b/c P&L fixtures;
  537 |   // job creation now goes exclusively through sales.create_job (ADR-133). Re-fixture or
  538 |   // rewrite against reseeded Acme jobs (JOB-20/JOB-21) if this coverage is needed again.
  539 |   test.skip('All Jobs screen shows bt-job-c with in_execution status', async ({ page }) => {
  540 |     await injectSession(page);
  541 |     await page.goto(BASE_URL);
  542 |     await page.waitForTimeout(500);
  543 |     await page.click('#nav-jobs');
  544 |     await page.waitForTimeout(600);
  545 |     const jobCards = page.locator('.job-card');
  546 |     const count = await jobCards.count();
  547 |     expect(count).toBeGreaterThan(0);
  548 |     // at least one card with in_execution
  549 |     const statuses = await page.locator('.job-card .tag').allTextContents();
  550 |     expect(statuses.some(s => s.toLowerCase().includes('execution') || s.toLowerCase().includes('billed'))).toBeTruthy();
  551 |   });
  552 | 
  553 |   test('Offers screen shows draft + sent + accepted chips', async ({ page }) => {
  554 |     await injectSession(page);
  555 |     await page.goto(BASE_URL);
  556 |     await page.waitForTimeout(500);
  557 |     await page.click('#nav-offers');
  558 |     await page.waitForTimeout(700);
  559 |     const statTags = await page.locator('#offers-list .tag').allTextContents();
  560 |     const normalised = statTags.map(s => s.toLowerCase());
  561 |     expect(normalised.some(s => s.includes('draft') || s.includes('sent') || s.includes('accepted'))).toBeTruthy();
  562 |   });
  563 | 
  564 |   test('Offer form has HSN/SAC input and SVC/MAT toggle', async ({ page }) => {
  565 |     await injectSession(page);
  566 |     await page.goto(BASE_URL);
  567 |     await page.waitForTimeout(400);
  568 |     await page.click('#nav-offers');
  569 |     await page.waitForTimeout(300);
  570 |     await page.click('#btn-offers-new');
  571 |     await page.waitForTimeout(400);
  572 |     await expect(page.locator('#screen-offer-form')).toBeVisible();
  573 |     await expect(page.locator('#off-po-number')).toBeVisible();
  574 |     await expect(page.locator('#off-po-date')).toBeVisible();
  575 |     await expect(page.locator('#off-company-select')).toBeVisible();
  576 |     // Add a line to see the line editor
  577 |     await page.fill('#off-client', 'Test Co');
  578 |     await page.click('button:has-text("+ Row")');
  579 |     await page.waitForTimeout(200);
  580 |     // HSN/SAC field should be visible in the line editor
  581 |     await expect(page.locator('#offer-lines-container input[list]').first()).toBeVisible();
  582 |   });
  583 | 
  584 |   test('Invoice form has company select + GST settings', async () => {
  585 |     // Structure-only — invoice form fields always exist in DOM
  586 |     const ctx = await request.newContext();
  587 |     const html = await (await ctx.get(BASE_URL)).text();
  588 |     await ctx.dispose();
  589 |     expect(html).toContain('id="inv-company-select"');
  590 |     expect(html).toContain('id="inv-gst-type"');
  591 |     expect(html).toContain('id="inv-po-no"');
  592 |     expect(html).toContain('id="inv-po-date"');
  593 |     expect(html).toContain('id="inv-po-amount"');
  594 |     expect(html).toContain('id="inv-lines-container"');
  595 |     expect(html).toContain('id="inv-agency-pct"');
  596 |     expect(html).toContain('id="inv-calc-grand"');
  597 |     expect(html).not.toContain('const COMPANIES =');
  598 |   });
  599 | 
  600 |   // SKIPPED (C7/ADR-133 2026-08-06): dev clean-slate deleted bt-job-b/c P&L fixtures;
  601 |   // job creation now goes exclusively through sales.create_job (ADR-133). Re-fixture or
  602 |   // rewrite against reseeded Acme jobs (JOB-20/JOB-21) if this coverage is needed again.
  603 |   test.skip('Challans tab visible on Job Detail', async ({ page }) => {
  604 |     await injectSession(page);
  605 |     await page.goto(BASE_URL);
  606 |     await page.waitForTimeout(800);
  607 |     await page.evaluate(() => openJob('bt-job-c'));
  608 |     await page.waitForTimeout(700);
```