Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.)
Expand Down Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
149 changes: 149 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading