# 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-issue-credit-note.spec.ts >> issues a full credit note for a paid invoice with one item by crediting the customer balance - Location: src/manager/tests/e2e/invoice/invoice-issue-credit-note.spec.ts:28:7 # Error details ``` Error: Failed to create invoice via API: 500 {"type":"about:blank","title":"Server Error","status":500,"detail":"An unexpected error occurred."} ``` # Test source ```ts 1 | import { APIRequestContext } from '@playwright/test'; 2 | import { 3 | fromInvoiceFetchApiResponse, 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 invoice writes under /v1; the billing API (modern Kinnovis billing) serves them under 19 | // /api/v1, and in modern mode the manager API creates invoices against Stripe prices the tenant no longer has. 20 | // Mirrors the manager SPA's kbInvoiceService: create, credit notes and mark-as-paid go through `request`, while 21 | // reads (invoice fetch, location payment methods) stay on `managerRequest` in both modes. 22 | export interface InvoiceApiClient { 23 | request: APIRequestContext; 24 | basePath: string; 25 | managerRequest: APIRequestContext; 26 | } 27 | 28 | const managerInvoiceBasePath = '/v1'; 29 | const billingInvoiceBasePath = '/api/v1'; 30 | 31 | export function resolveInvoiceApiClient( 32 | managerApiRequest: APIRequestContext, 33 | billingApiRequest: APIRequestContext 34 | ): InvoiceApiClient { 35 | return isModernBillingMode() 36 | ? { request: billingApiRequest, basePath: billingInvoiceBasePath, managerRequest: managerApiRequest } 37 | : { request: managerApiRequest, basePath: managerInvoiceBasePath, managerRequest: managerApiRequest }; 38 | } 39 | 40 | export async function createInvoiceViaApi(client: InvoiceApiClient, data: InvoiceCreateData): Promise { 41 | const defaultPaymentMethodIds = await fetchLocationDefaultPaymentMethodIdsViaApi( 42 | client.managerRequest, 43 | data.location.id 44 | ); 45 | 46 | const response = await client.request.post(`${client.basePath}/invoice`, { 47 | data: toInvoiceCreateApiPayload(data, defaultPaymentMethodIds), 48 | }); 49 | 50 | if (!response.ok()) { > 51 | throw new Error(`Failed to create invoice via API: ${response.status()} ${await response.text()}`); | ^ Error: Failed to create invoice via API: 500 {"type":"about:blank","title":"Server Error","status":500,"detail":"An unexpected error occurred."} 52 | } 53 | 54 | const { id } = ((await response.json()) as InvoiceCreateApiResponse).data; 55 | const body = await fetchInvoiceViaApi(client.managerRequest, id); 56 | 57 | return fromInvoiceFetchApiResponse(id, body, data); 58 | } 59 | 60 | async function fetchInvoiceViaApi(request: APIRequestContext, id: number): Promise { 61 | const response = await request.get(`/v1/invoice/${id}`); 62 | 63 | if (!response.ok()) { 64 | throw new Error(`Failed to fetch invoice ${id} via API: ${response.status()} ${await response.text()}`); 65 | } 66 | 67 | return (await response.json()) as InvoiceFetchApiResponse; 68 | } 69 | 70 | // Whether a credit note can be issued for the invoice right now — false while the invoice's charge is still 71 | // settling (pending payment) or the API forbids crediting it for any other reason. 72 | export async function fetchInvoiceCanBeCreditedViaApi(request: APIRequestContext, id: number): Promise { 73 | const body = await fetchInvoiceViaApi(request, id); 74 | 75 | return !body.data.has_pending_payment && body.data.actions.can_issue_credit_note; 76 | } 77 | 78 | // The credit endpoint identifies what to credit by invoice line-item ids, which only exist server-side — so 79 | // they are fetched here rather than carried on the Invoice domain type. The response is the updated invoice, 80 | // not the credit note — the credit note itself has no read endpoint, and its number is derivable via 81 | // toInvoiceCreditNoteNumber, so nothing from the response is needed. 82 | export async function issueInvoiceCreditNoteViaApi( 83 | client: InvoiceApiClient, 84 | id: number, 85 | data: InvoiceIssueCreditNoteData 86 | ): Promise { 87 | const body = await fetchInvoiceViaApi(client.managerRequest, id); 88 | 89 | const response = await client.request.post(`${client.basePath}/credit-note`, { 90 | data: toInvoiceIssueCreditNoteApiPayload(data, id, body.data.line_items), 91 | }); 92 | 93 | if (!response.ok()) { 94 | throw new Error( 95 | `Failed to issue credit note for invoice ${id} via API: ${response.status()} ${await response.text()}` 96 | ); 97 | } 98 | } 99 | 100 | // Marks an open invoice paid with an out-of-band cash payment — purely internal, no Stripe charge or email. 101 | // Used to stand up the 'paid' invoices the credit-note specs need without a real card charge. 102 | export async function markInvoiceAsPaidViaApi(client: InvoiceApiClient, id: number): Promise { 103 | const response = await client.request.put(`${client.basePath}/invoice/${id}/status-paid`, { 104 | data: { payment_date: dayjs().format('YYYY-MM-DD'), payment_type: 'cash' }, 105 | }); 106 | 107 | if (!response.ok()) { 108 | throw new Error(`Failed to mark invoice ${id} as paid via API: ${response.status()} ${await response.text()}`); 109 | } 110 | } 111 | ```