# 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: src/manager/tests/e2e/invoice/invoice-charge-customer.spec.ts >> charges the customer of an open invoice with a non-settling sepa that stays pending payment - Location: src/manager/tests/e2e/invoice/invoice-charge-customer.spec.ts:22:7 # Error details ``` Error: Failed to create invoice via API: 422 {"error":{"code":"internal.stripe","message":"No such price: '0938d4f3-038e-423c-a593-28fd027ee224'","details":[]}} ``` # Test source ```ts 1 | import { APIRequestContext } from '@playwright/test'; 2 | import { 3 | fromInvoiceCreateApiResponse, 4 | toInvoiceCreateApiPayload, 5 | toInvoiceIssueCreditNoteApiPayload, 6 | } from '@/manager/modules/invoice/invoice-mappers'; 7 | import { 8 | InvoiceCreateApiResponse, 9 | InvoiceCreateData, 10 | InvoiceFetchApiResponse, 11 | InvoiceIssueCreditNoteData, 12 | } from '@/manager/modules/invoice/invoice-types'; 13 | import { Invoice } from '@/manager/modules/invoice/invoice-types'; 14 | import { fetchLocationDefaultPaymentMethodIdsViaApi } from '@/manager/modules/location/location-api'; 15 | import { isModernBillingMode } from '@/shared/utils/billing-utils'; 16 | import dayjs from 'dayjs'; 17 | 18 | // The manager API serves invoices at /v1/invoice; the billing API (modern Kinnovis billing) serves mark-as-paid 19 | // at /api/v1/invoice, and the manager API rejects mark-as-paid for Kinnovis-billing tenants. Only mark-as-paid 20 | // goes through this client: create, fetch and credit notes stay on the manager API in both modes. 21 | export interface InvoiceApiClient { 22 | request: APIRequestContext; 23 | basePath: string; 24 | } 25 | 26 | const managerInvoiceBasePath = '/v1/invoice'; 27 | const billingInvoiceBasePath = '/api/v1/invoice'; 28 | 29 | export function resolveInvoiceApiClient( 30 | managerApiRequest: APIRequestContext, 31 | billingApiRequest: APIRequestContext 32 | ): InvoiceApiClient { 33 | return isModernBillingMode() 34 | ? { request: billingApiRequest, basePath: billingInvoiceBasePath } 35 | : { request: managerApiRequest, basePath: managerInvoiceBasePath }; 36 | } 37 | 38 | export async function createInvoiceViaApi(request: APIRequestContext, data: InvoiceCreateData): Promise { 39 | const defaultPaymentMethodIds = await fetchLocationDefaultPaymentMethodIdsViaApi(request, data.location.id); 40 | 41 | const response = await request.post('/v1/invoice', { 42 | data: toInvoiceCreateApiPayload(data, defaultPaymentMethodIds), 43 | }); 44 | 45 | if (!response.ok()) { > 46 | throw new Error(`Failed to create invoice via API: ${response.status()} ${await response.text()}`); | ^ Error: Failed to create invoice via API: 422 {"error":{"code":"internal.stripe","message":"No such price: '0938d4f3-038e-423c-a593-28fd027ee224'","details":[]}} 47 | } 48 | 49 | const body = (await response.json()) as InvoiceCreateApiResponse; 50 | 51 | return fromInvoiceCreateApiResponse(body, data); 52 | } 53 | 54 | async function fetchInvoiceViaApi(request: APIRequestContext, id: number): Promise { 55 | const response = await request.get(`/v1/invoice/${id}`); 56 | 57 | if (!response.ok()) { 58 | throw new Error(`Failed to fetch invoice ${id} via API: ${response.status()} ${await response.text()}`); 59 | } 60 | 61 | return (await response.json()) as InvoiceFetchApiResponse; 62 | } 63 | 64 | // Whether a credit note can be issued for the invoice right now — false while the invoice's charge is still 65 | // settling (pending payment) or the API forbids crediting it for any other reason. 66 | export async function fetchInvoiceCanBeCreditedViaApi(request: APIRequestContext, id: number): Promise { 67 | const body = await fetchInvoiceViaApi(request, id); 68 | 69 | return !body.data.has_pending_payment && body.data.actions.can_issue_credit_note; 70 | } 71 | 72 | // The credit endpoint identifies what to credit by invoice line-item ids, which only exist server-side — so 73 | // they are fetched here rather than carried on the Invoice domain type. The response is the updated invoice, 74 | // not the credit note — the credit note itself has no read endpoint, and its number is derivable via 75 | // toInvoiceCreditNoteNumber, so nothing from the response is needed. 76 | export async function issueInvoiceCreditNoteViaApi( 77 | request: APIRequestContext, 78 | id: number, 79 | data: InvoiceIssueCreditNoteData 80 | ): Promise { 81 | const body = await fetchInvoiceViaApi(request, id); 82 | 83 | const response = await request.post('/v1/credit-note', { 84 | data: toInvoiceIssueCreditNoteApiPayload(data, id, body.data.line_items), 85 | }); 86 | 87 | if (!response.ok()) { 88 | throw new Error( 89 | `Failed to issue credit note for invoice ${id} via API: ${response.status()} ${await response.text()}` 90 | ); 91 | } 92 | } 93 | 94 | // Marks an open invoice paid with an out-of-band cash payment — purely internal, no Stripe charge or email. 95 | // Used to stand up the 'paid' invoices the credit-note specs need without a real card charge. 96 | export async function markInvoiceAsPaidViaApi(client: InvoiceApiClient, id: number): Promise { 97 | const response = await client.request.put(`${client.basePath}/${id}/status-paid`, { 98 | data: { payment_date: dayjs().format('YYYY-MM-DD'), payment_type: 'cash' }, 99 | }); 100 | 101 | if (!response.ok()) { 102 | throw new Error(`Failed to mark invoice ${id} as paid via API: ${response.status()} ${await response.text()}`); 103 | } 104 | } 105 | ```