From af6456efa0578aef709767d7df59a1fdc1939ee5 Mon Sep 17 00:00:00 2001 From: Ben Kalsky Date: Wed, 13 May 2026 02:57:24 +0300 Subject: [PATCH] feat: add buildCreateDocumentPayload + normalizeCreateDocumentResponse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for POST /accounting/documents/create/ so consumers can issue SUMIT accounting documents (חשבון עסקה / Transaction Invoice, חשבונית מס, etc.) without going through a card charge. Useful for proposal/quote → invoice flows where the document is generated up front and payment is collected separately. - buildCreateDocumentPayload(params): assembles the request body. Items default TotalPrice to UnitPrice * Quantity; Customer.SearchMode defaults to 0 (this endpoint differs from the charge endpoints where SearchMode=2 is the typical upsert). - normalizeCreateDocumentResponse(response): surfaces success as eventType="document.created" with documentId, documentNumber, and documentDownloadUrl. Failures normalize to "document.failed" with redacted error messages. - SUMIT_DOCUMENT_TYPE.TransactionInvoice = 1. Other SUMIT document type codes are accepted as plain numbers. - currencyToSumitString helper — the documents endpoint takes literal "ILS"/"USD"/"EUR" strings, unlike the charge endpoints' numeric codes. - NormalizedSumitEvent gains documentNumber and documentDownloadUrl. Zero new runtime dependencies. 9 new tests; 23 total passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 16 +++ README.md | 61 ++++++++++ docs/API_REFERENCE.md | 59 ++++++++++ src/index.test.ts | 149 ++++++++++++++++++++++++ src/index.ts | 255 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 540 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e59add..9cbde8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this package are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- `buildCreateDocumentPayload(params)` — builder for `POST /accounting/documents/create/`. Issues SUMIT accounting documents (חשבון עסקה / חשבונית מס / חשבונית מס-קבלה / קבלה) without charging a card. +- `normalizeCreateDocumentResponse(response)` — surfaces successful creates as `eventType: "document.created"` with `documentId`, `documentNumber`, `documentDownloadUrl`, `customerId`. Failures surface as `eventType: "document.failed"`. +- `SUMIT_DOCUMENT_TYPE` const exposing `TransactionInvoice = 1`. Other SUMIT document type codes can be passed directly as numbers. +- `currencyToSumitString(currency)` helper — the documents endpoint takes literal `"ILS"`/`"USD"`/`"EUR"` strings rather than the numeric codes used by the charge endpoints. +- `documentNumber` and `documentDownloadUrl` fields on `NormalizedSumitEvent`. +- Type exports: `BuildCreateDocumentPayloadParams`, `SumitCreateDocumentPayload`, `CreateDocumentItem`, `CreateDocumentCustomer`, `CreateDocumentSendByEmail`. + +### Changed + +- `SumitNormalizedEventType` adds `"document.created"` and `"document.failed"`. +- README and API reference document the new endpoint and helpers. + ## [0.2.0] - 2026-05-02 ### Added diff --git a/README.md b/README.md index 896dc4f..a7596b5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ Companion package: [`sumit-react`](https://github.com/Digitizers/sumit-react) - [Install](#install) - [Build a one-off charge payload](#build-a-one-off-charge-payload) - [Build a recurring-charge payload](#build-a-recurring-charge-payload) +- [Build a create-document payload](#build-a-create-document-payload) - [Normalize a charge response](#normalize-a-charge-response) +- [Normalize a create-document response](#normalize-a-create-document-response) - [Normalize a SUMIT trigger / webhook payload](#normalize-a-sumit-trigger--webhook-payload) - [Safety](#safety) - [Development](#development) @@ -105,6 +107,38 @@ const payload = buildRecurringChargePayload({ --- +## Build a create-document payload + +Issue a SUMIT accounting document (חשבון עסקה / Transaction Invoice) without charging a card — useful when a proposal/quote is accepted and you want to hand the customer a pre-payment invoice. `POST` the body to `https://api.sumit.co.il/accounting/documents/create/`. + +```ts +import { buildCreateDocumentPayload, SUMIT_DOCUMENT_TYPE } from "sumit-api"; + +const payload = buildCreateDocumentPayload({ + companyId: 123, + apiKey: process.env.SUMIT_API_KEY!, + documentType: SUMIT_DOCUMENT_TYPE.TransactionInvoice, // 1 + customer: { + externalIdentifier: "client_42", + name: "Acme Ltd", + emailAddress: "billing@example.com", + taxId: "514999000", // ת.ז. / ח.פ. — mapped to CompanyNumber + }, + items: [ + { name: "Logo design", description: "Includes 3 revisions", unitPrice: 1500, quantity: 1 }, + { name: "Development hours", unitPrice: 300, quantity: 8 }, + ], + currency: "ILS", + vatIncluded: false, // unit prices are net; SUMIT adds VAT + language: "he", + sendByEmail: { emailAddress: "billing@example.com" }, // optional +}); +``` + +`SUMIT_DOCUMENT_TYPE` only lists values this package has actively verified. SUMIT exposes many more document type codes — pass any number directly via the `documentType` field. + +--- + ## Normalize a charge response `normalizeChargeResponse` handles both one-off and recurring response shapes — a `recurring.charged` event is surfaced only when SUMIT returns a `RecurringCustomerItemIDs[*]`. (`normalizeRecurringChargeResponse` remains exported as an alias.) @@ -139,6 +173,33 @@ A successful SUMIT charge response typically includes: --- +## Normalize a create-document response + +```ts +import { normalizeCreateDocumentResponse } from "sumit-api"; + +const event = normalizeCreateDocumentResponse(sumitResponse); + +if (event.ok && event.eventType === "document.created") { + // Persist event.documentId / event.documentNumber / event.documentDownloadUrl. +} + +if (event.eventType === "document.failed") { + // event.userErrorMessage is safe to display; event.technicalErrorDetails is redacted. +} +``` + +A successful create-document response surfaces: + +| Field | Source | +| --------------------- | -------------------------------------------------------- | +| `documentId` | `Data.DocumentID` / `DocumentID` | +| `documentNumber` | `Data.DocumentNumber` / `Data.Document.Number` | +| `documentDownloadUrl` | `Data.DocumentDownloadURL` / `Data.Document.DownloadURL` | +| `customerId` | `Data.CustomerID` / `Data.Customer.ID` | + +--- + ## Normalize a SUMIT trigger / webhook payload ```ts diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index f138962..72bbb06 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -115,6 +115,65 @@ Notes: Successful payloads observed in smoke tests include `Payment.ValidPayment === true`, `Payment.Status === "000"`, `Payment.ID`, `CustomerID`, `DocumentID`, and `RecurringCustomerItemIDs[0]`. +## `POST /accounting/documents/create/` + +Issues a SUMIT accounting document (חשבון עסקה / חשבונית מס / חשבונית מס-קבלה / קבלה) without charging a card. Built by `buildCreateDocumentPayload`: + +```ts +{ + Credentials: { CompanyID, APIKey }, + Details: { + Type: number, // SUMIT document type code; 1 = חשבון עסקה + Customer: { + SearchMode: 0 | 1 | 2, // 0 = match by ID (default for this endpoint) + Name: string, + EmailAddress?: string, + Phone?: string, + ExternalIdentifier?: string, + ID?: string, + CompanyNumber?: string, // ת.ז. / ח.פ. + Address?: string, + City?: string, + ZipCode?: string, + NoVAT?: boolean, + }, + SendByEmail?: { EmailAddress, Original, SendAsPaymentRequest }, + Language?: string, // e.g. "he" / "en" + Currency?: "ILS" | "USD" | "EUR", + Description?: string, + ExternalReference?: string, + Date?: string, // ISO date + DueDate?: string, + IsDraft?: boolean, + }, + Items: [{ + Quantity: number, + UnitPrice: number, + TotalPrice: number, // defaults to UnitPrice * Quantity + VAT?: number, // optional per-line override + Item: { + Name: string, + Description?: string, + SKU?: string, + ExternalIdentifier?: string, + SearchMode: 0 | 1 | 2, + }, + }], + Payments: [], + VATIncluded: boolean, + VATPerItem?: boolean, + VATRate?: number, + ResponseLanguage?: string, +} +``` + +Notes: + +- Unlike the charge endpoints, the documents endpoint takes `Currency` as the literal string code (`"ILS"` / `"USD"` / `"EUR"`), not the numeric code. The helper `currencyToSumitString` handles the mapping. +- `Payments: []` for a חשבון עסקה — no payment has been collected yet. Use a different document type (e.g. חשבונית מס-קבלה) and a populated `Payments[]` to record an actual payment. +- Successful responses surface `Data.DocumentID`, `Data.DocumentNumber`, and (when SUMIT generates one) `Data.DocumentDownloadURL`. Pass the response to `normalizeCreateDocumentResponse` to extract these as `document.created`. +- A failed response surfaces as `document.failed` with redacted `userErrorMessage` / `technicalErrorDetails`. + ## Related endpoints not wrapped directly | Endpoint | Purpose | diff --git a/src/index.test.ts b/src/index.test.ts index c161563..0e4c16e 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,11 +1,15 @@ import { describe, expect, it } from "vitest"; import { + buildCreateDocumentPayload, buildOneOffChargePayload, buildRecurringChargePayload, + currencyToSumitString, normalizeChargeResponse, + normalizeCreateDocumentResponse, normalizeRecurringChargeResponse, normalizeSumitIncomingPayload, redactSumitPayload, + SUMIT_DOCUMENT_TYPE, } from "./index"; describe("@deepclaw/sumit", () => { @@ -342,6 +346,151 @@ describe("@deepclaw/sumit", () => { expect(event.eventType).toBeDefined(); }); + it("builds a SUMIT /accounting/documents/create/ payload for a חשבון עסקה", () => { + const payload = buildCreateDocumentPayload({ + companyId: 123, + apiKey: "api-key", + documentType: SUMIT_DOCUMENT_TYPE.TransactionInvoice, + customer: { + externalIdentifier: "client-1", + name: "אקמה בע״מ", + emailAddress: "billing@example.invalid", + taxId: "514999000", + }, + items: [ + { + name: "עיצוב לוגו", + description: "כולל 3 סבבי תיקונים", + unitPrice: 1500, + quantity: 1, + }, + { + name: "שעות פיתוח", + unitPrice: 300, + quantity: 8, + }, + ], + currency: "ILS", + vatIncluded: false, + language: "he", + }); + + expect(payload).toEqual({ + Credentials: { CompanyID: 123, APIKey: "api-key" }, + Details: { + Type: 1, + Customer: { + SearchMode: 0, + Name: "אקמה בע״מ", + EmailAddress: "billing@example.invalid", + ExternalIdentifier: "client-1", + CompanyNumber: "514999000", + }, + Language: "he", + Currency: "ILS", + }, + Items: [ + { + Quantity: 1, + UnitPrice: 1500, + TotalPrice: 1500, + Item: { Name: "עיצוב לוגו", Description: "כולל 3 סבבי תיקונים", SearchMode: 0 }, + }, + { + Quantity: 8, + UnitPrice: 300, + TotalPrice: 2400, + Item: { Name: "שעות פיתוח", SearchMode: 0 }, + }, + ], + Payments: [], + VATIncluded: false, + }); + }); + + it("includes SendByEmail when requested and maps currency strings", () => { + const payload = buildCreateDocumentPayload({ + companyId: 7, + apiKey: "k", + documentType: 1, + customer: { name: "C" }, + items: [{ name: "Item", unitPrice: 10 }], + currency: "USD", + sendByEmail: { emailAddress: "c@example.invalid", sendAsPaymentRequest: true }, + }); + + expect(payload.Details.Currency).toBe("USD"); + expect(payload.Details.SendByEmail).toEqual({ + EmailAddress: "c@example.invalid", + Original: true, + SendAsPaymentRequest: true, + }); + }); + + it("rejects an empty items[] array", () => { + expect(() => + buildCreateDocumentPayload({ + companyId: 1, + apiKey: "k", + documentType: 1, + customer: { name: "C" }, + items: [], + }), + ).toThrow(/items\[\] must not be empty/); + }); + + it("normalizes a successful /accounting/documents/create/ response", () => { + const event = normalizeCreateDocumentResponse({ + Status: "Success", + Data: { + DocumentID: "doc-42", + DocumentNumber: "2026001", + DocumentDownloadURL: "https://app.sumit.co.il/accounting/documents/2026001", + CustomerID: "cust-7", + }, + }); + + expect(event.ok).toBe(true); + expect(event.eventType).toBe("document.created"); + expect(event.documentId).toBe("doc-42"); + expect(event.documentNumber).toBe("2026001"); + expect(event.documentDownloadUrl).toBe("https://app.sumit.co.il/accounting/documents/2026001"); + expect(event.customerId).toBe("cust-7"); + }); + + it("normalizes a failed create-document response and redacts sensitive text", () => { + const event = normalizeCreateDocumentResponse({ + Status: "Error", + UserErrorMessage: "השגיאה נכשלה", + TechnicalErrorDetails: "Upay_30001419 invalid token=abc", + }); + + expect(event.ok).toBe(false); + expect(event.eventType).toBe("document.failed"); + expect(event.technicalErrorDetails).not.toContain("Upay_30001419"); + expect(event.technicalErrorDetails).not.toContain("abc"); + expect(event.diagnostic).toBeDefined(); + }); + + it("redacts API key and customer email when logging a built document payload", () => { + const payload = buildCreateDocumentPayload({ + companyId: 1, + apiKey: "super-secret", + documentType: 1, + customer: { name: "C", emailAddress: "c@example.invalid" }, + items: [{ name: "Item", unitPrice: 10 }], + }); + const redacted = redactSumitPayload(payload) as { Credentials: { APIKey: string }; Details: { Customer: { EmailAddress?: string } } }; + expect(redacted.Credentials.APIKey).toBe("[REDACTED]"); + expect(redacted.Details.Customer.EmailAddress).toBe("[REDACTED]"); + }); + + it("currencyToSumitString maps codes and labels", () => { + expect(currencyToSumitString("ILS")).toBe("ILS"); + expect(currencyToSumitString(1)).toBe("USD"); + expect(currencyToSumitString("EUR")).toBe("EUR"); + }); + it("preserves non-citizen 9-digit numbers in diagnostic text and redacts citizen IDs in context", () => { const passthrough = redactSumitPayload({ TechnicalErrorDetails: "Document 123456789 was not found", diff --git a/src/index.ts b/src/index.ts index 6fea1ac..3a2c81a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,18 @@ export type SumitNormalizedEventType = | "recurring.charged" | "recurring.cancelled" | "invoice.created" + | "document.created" + | "document.failed" | "sumit.trigger.unmapped"; +// Known SUMIT document type codes. `TransactionInvoice` (1) is the +// pre-payment "חשבון עסקה". Other numeric codes (tax invoice, receipt, +// tax-invoice-receipt, etc.) accepted by SUMIT can be passed as plain +// numbers — this object only lists values we have actively verified. +export const SUMIT_DOCUMENT_TYPE = { + TransactionInvoice: 1, +} as const; + export interface SumitDiagnostic { hasData: boolean; dataKeys: string[]; @@ -101,6 +111,8 @@ export interface NormalizedSumitEvent { paymentId?: string; customerId?: string; documentId?: string; + documentNumber?: string; + documentDownloadUrl?: string; recurringItemId?: string; amount?: number; currency?: "ILS" | "USD" | "EUR" | string; @@ -112,6 +124,121 @@ export interface NormalizedSumitEvent { diagnostic?: SumitDiagnostic; } +export interface CreateDocumentItem { + name: string; + description?: string; + unitPrice: number; + quantity?: number; + // Optional override; defaults to unitPrice * quantity. + totalPrice?: number; + // Optional explicit per-line VAT amount. + vat?: number; + sku?: string; + externalIdentifier?: string; +} + +export interface CreateDocumentCustomer { + name: string; + emailAddress?: string; + phone?: string; + externalIdentifier?: string; + // SUMIT internal customer ID (when known). + id?: string; + // Mapped to CompanyNumber — Israeli ת.ז. / ח.פ. + taxId?: string; + address?: string; + city?: string; + zipCode?: string; + noVAT?: boolean; + // SUMIT customer search mode. 0 = match by ID only (default for this endpoint), + // 2 = upsert by ExternalIdentifier (matches the charge endpoints). + searchMode?: 0 | 1 | 2; +} + +export interface CreateDocumentSendByEmail { + emailAddress: string; + original?: boolean; + sendAsPaymentRequest?: boolean; +} + +export interface BuildCreateDocumentPayloadParams { + companyId: number; + apiKey: string; + // SUMIT document type code. Use SUMIT_DOCUMENT_TYPE.TransactionInvoice (1) + // for חשבון עסקה. Other codes from SUMIT's enum can be passed as plain numbers. + documentType: number; + customer: CreateDocumentCustomer; + items: CreateDocumentItem[]; + // Defaults to "ILS". Accepts the same shape as the charge endpoints but + // SUMIT documents take the literal string code, not the numeric one. + currency?: SumitCurrency; + // Whether the UnitPrice values already include VAT. Defaults to true to + // match SUMIT's API default; pass false if your items are net. + vatIncluded?: boolean; + vatPerItem?: boolean; + vatRate?: number; + language?: string; + description?: string; + externalReference?: string; + date?: string; + dueDate?: string; + sendByEmail?: CreateDocumentSendByEmail; + isDraft?: boolean; + responseLanguage?: string; +} + +type SumitCustomerSearchMode = 0 | 1 | 2; + +export interface SumitCreateDocumentPayload { + Credentials: { CompanyID: number; APIKey: string }; + Details: { + Type: number; + Customer: { + SearchMode: SumitCustomerSearchMode; + Name: string; + EmailAddress?: string; + Phone?: string; + ExternalIdentifier?: string; + ID?: string; + CompanyNumber?: string; + Address?: string; + City?: string; + ZipCode?: string; + NoVAT?: boolean; + }; + SendByEmail?: { + EmailAddress: string; + Original: boolean; + SendAsPaymentRequest: boolean; + }; + Language?: string; + Currency?: "ILS" | "USD" | "EUR" | string; + Description?: string; + ExternalReference?: string; + Date?: string; + DueDate?: string; + IsDraft?: boolean; + }; + Items: Array<{ + Quantity: number; + UnitPrice: number; + TotalPrice: number; + VAT?: number; + Item: { + Name: string; + Description?: string; + SKU?: string; + ExternalIdentifier?: string; + SearchMode: SumitCustomerSearchMode; + }; + }>; + Payments: never[]; + VATIncluded: boolean; + VATPerItem?: boolean; + VATRate?: number; + ResponseLanguage?: string; +} + type UnknownRecord = Record; const SENSITIVE_KEY_PATTERN = /(^|_)(api(public)?key|singleusetoken|token|authorization|secret|password|cvv|citizenid|card(mask|pattern|token|expiration)?|cardowner(name|socialid)?|creditcard(_.*)?|directdebit(_.*)?|authnumber|emailaddress|phone|resultrecord|documentdownloadurl)$/i; @@ -123,6 +250,11 @@ export function currencyToSumitCode(currency: SumitCurrency): 0 | 1 | 2 { throw new Error(`Unsupported SUMIT currency: ${String(currency)}`); } +export function currencyToSumitString(currency: SumitCurrency): "ILS" | "USD" | "EUR" { + const code = currencyToSumitCode(currency); + return code === 0 ? "ILS" : code === 1 ? "USD" : "EUR"; +} + export function currencyFromSumitCode(currency: unknown): "ILS" | "USD" | "EUR" | string | undefined { if (currency === 0 || currency === "0" || currency === "ILS") return "ILS"; if (currency === 1 || currency === "1" || currency === "USD") return "USD"; @@ -168,6 +300,129 @@ export function buildRecurringChargePayload(params: BuildRecurringChargePayloadP }; } +export function buildCreateDocumentPayload(params: BuildCreateDocumentPayloadParams): SumitCreateDocumentPayload { + if (!params.items.length) { + throw new Error("buildCreateDocumentPayload: items[] must not be empty"); + } + + const customer = compact({ + SearchMode: (params.customer.searchMode ?? 0) as SumitCustomerSearchMode, + Name: params.customer.name, + EmailAddress: params.customer.emailAddress, + Phone: params.customer.phone, + ExternalIdentifier: params.customer.externalIdentifier, + ID: params.customer.id, + CompanyNumber: params.customer.taxId, + Address: params.customer.address, + City: params.customer.city, + ZipCode: params.customer.zipCode, + NoVAT: params.customer.noVAT, + }) as SumitCreateDocumentPayload["Details"]["Customer"]; + + const sendByEmail = params.sendByEmail + ? { + EmailAddress: params.sendByEmail.emailAddress, + Original: params.sendByEmail.original ?? true, + SendAsPaymentRequest: params.sendByEmail.sendAsPaymentRequest ?? false, + } + : undefined; + + const details = compact({ + Type: params.documentType, + Customer: customer, + SendByEmail: sendByEmail, + Language: params.language, + Currency: params.currency ? currencyToSumitString(params.currency) : undefined, + Description: params.description, + ExternalReference: params.externalReference, + Date: params.date, + DueDate: params.dueDate, + IsDraft: params.isDraft, + }) as SumitCreateDocumentPayload["Details"]; + + const items: SumitCreateDocumentPayload["Items"] = params.items.map((item) => { + const quantity = item.quantity ?? 1; + const totalPrice = item.totalPrice ?? round2(item.unitPrice * quantity); + return { + Quantity: quantity, + UnitPrice: item.unitPrice, + TotalPrice: totalPrice, + ...(item.vat !== undefined ? { VAT: item.vat } : {}), + Item: compact({ + Name: item.name, + Description: item.description, + SKU: item.sku, + ExternalIdentifier: item.externalIdentifier, + SearchMode: 0 as SumitCustomerSearchMode, + }) as SumitCreateDocumentPayload["Items"][number]["Item"], + }; + }); + + return compact({ + Credentials: { CompanyID: params.companyId, APIKey: params.apiKey }, + Details: details, + Items: items, + Payments: [] as never[], + VATIncluded: params.vatIncluded ?? true, + VATPerItem: params.vatPerItem, + VATRate: params.vatRate, + ResponseLanguage: params.responseLanguage, + }) as SumitCreateDocumentPayload; +} + +export function normalizeCreateDocumentResponse(response: unknown): NormalizedSumitEvent { + if (!isRecord(response)) { + return unmappedDiagnostic(null); + } + + const data = getRecord(response.Data) ?? response; + const documentId = + stringValue(response.DocumentID) ?? + stringValue(data.DocumentID) ?? + stringValue(getRecord(data.Document)?.ID); + const documentNumber = + stringValue(data.DocumentNumber) ?? + stringValue(response.DocumentNumber) ?? + stringValue(getRecord(data.Document)?.Number); + const documentDownloadUrl = + stringValue(data.DocumentDownloadURL) ?? + stringValue(response.DocumentDownloadURL) ?? + stringValue(getRecord(data.Document)?.DownloadURL); + const customerId = + stringValue(response.CustomerID) ?? + stringValue(data.CustomerID) ?? + stringValue(getRecord(data.Customer)?.ID); + + const status = stringValue(response.Status); + const userErrorMessage = safeText(response.UserErrorMessage); + const technicalErrorDetails = safeText(response.TechnicalErrorDetails); + const failed = isFailedStatus({ status, userErrorMessage, technicalErrorDetails }); + const succeeded = !failed && Boolean(documentId) && (status === undefined || status === "Success" || status === "0" || status === "000"); + + if (!documentId && !failed && !userErrorMessage && !technicalErrorDetails) { + return unmappedDiagnostic(response); + } + + const eventType: SumitNormalizedEventType = failed ? "document.failed" : succeeded ? "document.created" : "sumit.trigger.unmapped"; + + return compact({ + ok: failed ? false : succeeded ? true : null, + eventType, + documentId, + documentNumber, + documentDownloadUrl, + customerId, + status, + userErrorMessage, + technicalErrorDetails, + ...(eventType === "sumit.trigger.unmapped" || failed ? { diagnostic: diagnosticFor(response) } : {}), + }); +} + +function round2(value: number): number { + return Math.round(value * 100) / 100; +} + function baseChargeEnvelope(params: BaseChargeParams): BaseChargePayload { return { Credentials: {