pricing.spec.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import { expect, type Page, test } from '@playwright/test'
  2. import { setTax } from '../../support/settings'
  3. import {
  4. addLabor,
  5. addPart,
  6. lastPartUnitPrice,
  7. newWorkOrder,
  8. saveWorkOrder,
  9. seededVehicleUrl,
  10. setDiscount,
  11. shareLink,
  12. totalsRow,
  13. } from '../../support/work-order'
  14. /**
  15. * The money on a work order, under every tax setting the workshop can choose.
  16. *
  17. * One job, priced the same way each time: two parts at a cost of 100 with a
  18. * 50% markup, and an hour and a half of labour at 400. What that comes to on
  19. * the editor, on the shared invoice and in the PDF is pinned here to the
  20. * cent, so a change anywhere in the pricing path that moves a customer's
  21. * invoice fails a test before it ships.
  22. *
  23. * Serial, because the tax settings are the workshop's and each scenario
  24. * creates its work order after changing them. The settings are put back at
  25. * the end.
  26. */
  27. test.describe.configure({ mode: 'serial' })
  28. const stamp = Date.now()
  29. /** The standard job: parts 2 × 150 = 300, labour 1.5 × 400 = 600, before tax or discount 900. */
  30. async function priceTheJob(page: Page, vehicleUrl: string, title: string): Promise<string> {
  31. const url = await newWorkOrder(page, vehicleUrl, title)
  32. await addPart(page, {
  33. name: `E2E brake pads ${stamp}`,
  34. quantity: 2,
  35. cost: 100,
  36. markupPercent: 50,
  37. })
  38. // The markup decides the price: 100 plus half is 150.
  39. expect(await lastPartUnitPrice(page)).toBe('150')
  40. await addLabor(page, { description: 'Replace front brake pads', hours: 1.5, rate: 400 })
  41. await saveWorkOrder(page)
  42. return url
  43. }
  44. async function expectTotals(page: Page, lines: Record<string, string>) {
  45. for (const [label, figure] of Object.entries(lines)) {
  46. await expect(totalsRow(page, label), `${label} on the work order`).toContainText(figure)
  47. }
  48. }
  49. /** Opens the customer's copy and checks the same figures print there. */
  50. async function expectOnSharedInvoice(page: Page, workOrderUrl: string, texts: (string | RegExp)[]) {
  51. await page.goto(workOrderUrl)
  52. const url = await shareLink(page)
  53. await page.goto(url)
  54. // The sheet keeps a hidden copy of itself for measuring; only the drawn one counts.
  55. for (const text of texts) {
  56. await expect(
  57. page.getByText(text).filter({ visible: true }).first(),
  58. `${text} on the shared invoice`
  59. ).toBeVisible()
  60. }
  61. }
  62. async function expectPdf(page: Page, workOrderUrl: string) {
  63. const id = workOrderUrl.split('/').pop()
  64. const response = await page.request.get(`/api/protected/services/${id}/pdf`)
  65. expect(response.status(), 'the invoice PDF renders').toBe(200)
  66. expect(response.headers()['content-type']).toContain('application/pdf')
  67. expect((await response.body()).length).toBeGreaterThan(1_000)
  68. }
  69. let vehicleUrl = ''
  70. let exclusiveJob = ''
  71. test.beforeAll(async ({ browser }) => {
  72. const page = await browser.newPage({ storageState: 'e2e/.auth/owner.json' })
  73. vehicleUrl = await seededVehicleUrl(page)
  74. await page.close()
  75. })
  76. test.describe('tax added on top', () => {
  77. test('a 25% rate is added to net lines', async ({ page }) => {
  78. await setTax(page, { enabled: true, rate: 25, inclusive: false })
  79. exclusiveJob = await priceTheJob(page, vehicleUrl, `E2E exclusive ${stamp}`)
  80. await expectTotals(page, {
  81. Parts: '$300.00',
  82. Labor: '$600.00',
  83. Subtotal: '$900.00',
  84. Tax: '$225.00',
  85. Total: '$1,125.00',
  86. })
  87. await expect(page.getByLabel('Invoice Number')).not.toHaveValue('')
  88. await expectOnSharedInvoice(page, exclusiveJob, [
  89. '$900.00',
  90. /Tax \(25%\)/,
  91. '$225.00',
  92. '$1,125.00',
  93. ])
  94. await expectPdf(page, exclusiveJob)
  95. })
  96. test('a percentage discount comes off before the tax', async ({ page }) => {
  97. await page.goto(exclusiveJob)
  98. await setDiscount(page, 'Percentage', 10)
  99. await saveWorkOrder(page)
  100. await expectTotals(page, {
  101. Subtotal: '$900.00',
  102. Discount: '-$90.00',
  103. Tax: '$202.50',
  104. Total: '$1,012.50',
  105. })
  106. await expectOnSharedInvoice(page, exclusiveJob, ['$202.50', '$1,012.50'])
  107. })
  108. test('a fixed discount does the same', async ({ page }) => {
  109. await page.goto(exclusiveJob)
  110. await setDiscount(page, 'Fixed', 100)
  111. await saveWorkOrder(page)
  112. await expectTotals(page, {
  113. Discount: '-$100.00',
  114. Tax: '$200.00',
  115. Total: '$1,000.00',
  116. })
  117. await expectOnSharedInvoice(page, exclusiveJob, ['$200.00', '$1,000.00'])
  118. })
  119. })
  120. test.describe('tax included in the prices', () => {
  121. test('a 25% rate is taken out of gross lines and the total is what was typed', async ({
  122. page,
  123. }) => {
  124. await setTax(page, { enabled: true, rate: 25, inclusive: true, label: 'MVA' })
  125. const job = await priceTheJob(page, vehicleUrl, `E2E inclusive ${stamp}`)
  126. // The editor shows the net figures; the customer pays what was typed.
  127. await expectTotals(page, {
  128. Subtotal: '$720.00',
  129. Tax: '$180.00',
  130. Total: '$900.00',
  131. })
  132. await expectOnSharedInvoice(page, job, [/MVA/, /25%/, '$180.00', '$900.00'])
  133. await expectPdf(page, job)
  134. })
  135. })
  136. test.describe('no tax at all', () => {
  137. test('a workshop with tax off prints no tax line', async ({ page }) => {
  138. await setTax(page, { enabled: false })
  139. const job = await priceTheJob(page, vehicleUrl, `E2E untaxed ${stamp}`)
  140. await expectTotals(page, { Subtotal: '$900.00', Total: '$900.00' })
  141. await expect(totalsRow(page, 'Tax')).toHaveCount(0)
  142. await expectOnSharedInvoice(page, job, ['$900.00'])
  143. await expect(page.getByText(/^Tax/).filter({ visible: true })).toHaveCount(0)
  144. })
  145. test('the tax settings are put back', async ({ page }) => {
  146. await setTax(page, { enabled: true, rate: 25, inclusive: false, label: '' })
  147. })
  148. })