@kevludwig/lexware-office 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/contacts.d.ts +8 -0
- package/dist/contacts.js +4 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/money.d.ts +5 -1
- package/dist/money.js +4 -2
- package/dist/sales-documents.d.ts +119 -17
- package/dist/sales-documents.js +137 -19
- package/dist/settings.d.ts +68 -0
- package/dist/settings.js +60 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ A failed request throws `LexwareApiError` with `status` and `responseBody`; `des
|
|
|
36
36
|
|
|
37
37
|
**Contacts** — `getContact`, `findContactByNumber`, `findContactsByName`, `resolveContactByName` (exact name first, then the single candidate), `contactEmail`, `createCustomer`.
|
|
38
38
|
|
|
39
|
-
**Sales documents** — `createSalesDocument` for quotations, order confirmations, and invoices, with `finalize`, a total discount, `precedingSalesVoucherId` for a document chain, `introduction` and `remark`, and line items that are priced (with an optional `description
|
|
39
|
+
**Sales documents** — `createSalesDocument` for quotations, order confirmations, and invoices, with `finalize`, `voucherDate`, `shippingConditions` (service or delivery date or period), `paymentConditions` (`paymentTermLabel` phrases one as Lexware does), `taxConditions` (net, gross, and the vat-free types), a total discount in EUR or percent, `language`, `printLayoutId`, `deliveryTerms` (order confirmations), `xRechnung` (invoices), `precedingSalesVoucherId` for a document chain, `introduction` and `remark`, and line items that are priced (with an optional `description`, `discountPercentage`, `grossPrice`, an `article` reference; `optional` and `alternatives` on quotations) or `type: "text"` headings; `salesDocumentProblems` names what the API would drop or reject before anything is sent; `SALES_TEXT_LIMITS` holds the API's text lengths; `getSalesDocument` (the lines that count, optional lines apart, totals, discounts, customer), `invoiceExists`, `findRecentSalesDocuments`, `findDuplicateSalesDocument` (same net total, same customer, within a window), `isPursueRejection` for the 406 an unusable chain answers with. Account settings a document refers to: `listPrintLayouts` / `resolvePrintLayout`, `listPaymentConditions` / `defaultPaymentCondition`, `getProfile`, `listCountries` (with their tax classification), `getArticle`.
|
|
40
40
|
|
|
41
41
|
**Purchase invoices** — `createVoucher`, `uploadVoucherFile` for the original, `findVouchers`, `findPurchaseInvoiceByNumber` for a document already captured, `findVendorCategoryHistory` for how this vendor's invoices were booked so far.
|
|
42
42
|
|
package/dist/contacts.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export interface Contact {
|
|
|
14
14
|
number?: number;
|
|
15
15
|
isCustomer: boolean;
|
|
16
16
|
isVendor: boolean;
|
|
17
|
+
/** A company rather than a person. */
|
|
18
|
+
isCompany?: boolean;
|
|
19
|
+
/** The company's VAT ID (USt-IdNr.) — needed for the § 13b and intra-community tax types. */
|
|
20
|
+
vatRegistrationId?: string;
|
|
21
|
+
/** „Steuerfreie Rechnung erlauben" — needed for the third-country tax types. */
|
|
22
|
+
allowTaxFreeInvoices?: boolean;
|
|
23
|
+
/** ISO code of the first billing address. */
|
|
24
|
+
countryCode?: string;
|
|
17
25
|
}
|
|
18
26
|
/** A contact by its id. */
|
|
19
27
|
export declare function getContact(client: LexwareClient, id: string, signal?: AbortSignal): Promise<Contact>;
|
package/dist/contacts.js
CHANGED
|
@@ -16,6 +16,10 @@ function toContact(raw) {
|
|
|
16
16
|
number: raw.roles?.customer?.number ?? raw.roles?.vendor?.number,
|
|
17
17
|
isCustomer: raw.roles?.customer !== undefined,
|
|
18
18
|
isVendor: raw.roles?.vendor !== undefined,
|
|
19
|
+
isCompany: raw.company !== undefined,
|
|
20
|
+
...(raw.company?.vatRegistrationId?.trim() ? { vatRegistrationId: raw.company.vatRegistrationId.trim() } : {}),
|
|
21
|
+
...(raw.company?.allowTaxFreeInvoices !== undefined ? { allowTaxFreeInvoices: raw.company.allowTaxFreeInvoices } : {}),
|
|
22
|
+
...(raw.addresses?.billing?.[0]?.countryCode ? { countryCode: raw.addresses.billing[0].countryCode } : {}),
|
|
19
23
|
};
|
|
20
24
|
}
|
|
21
25
|
/** A contact by its id. */
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export { downloadInvoicePdf, findOverdueInvoice, findOverdueInvoices, type Overd
|
|
|
5
5
|
export { DEFAULT_REMINDER_POLICY, daysPastReminderDue, earliestReminderDate, isDueImmediately, isOpenForReminder, reminderDueDate, reminderLedger, selectDueReminders, type DueReminder, type ReminderHold, type ReminderLedger, type ReminderPolicy, type ReminderRecord, type ReminderSelection, type ReminderStatus, mailContentSha256, paymentReminderEffect, sendReminder, type PaymentReminderEffect, type ReminderTarget, type SendReminderOutcome, } from "./reminders.ts";
|
|
6
6
|
export { MailNotSentError, defaultReminderMail, reminderSubject, type ReminderMail, type ReminderMailData, type ReminderSender, type RenderReminderMail, type SendReminderMail, } from "./reminder-mail.ts";
|
|
7
7
|
export { keySegment, memoryStore, type JsonStore } from "./store.ts";
|
|
8
|
-
export { DUPLICATE_TOTAL_TOLERANCE, ITEMS_TOTAL_TOLERANCE, netTotal, round2, taxFromGross, taxFromNet, type PricedLine, } from "./money.ts";
|
|
8
|
+
export { DUPLICATE_TOTAL_TOLERANCE, ITEMS_TOTAL_TOLERANCE, lineNet, netTotal, round2, taxFromGross, taxFromNet, type PricedLine, } from "./money.ts";
|
|
9
9
|
export { REVERSE_CHARGE_TAX_RATE, isReverseChargeCategory, listPostingCategories, resolveCategory, type PostingCategory, } from "./categories.ts";
|
|
10
10
|
export { createVoucher, findPurchaseInvoiceByNumber, findVendorCategoryHistory, findVouchers, uploadVoucherFile, voucherFileIds, type CategoryHistory, type CreateVoucherOptions, type CreatedVoucher, type VoucherItem, type VoucherListEntry, type VoucherListFilter, type VoucherStatus, type VoucherTaxType, type VoucherType, } from "./vouchers.ts";
|
|
11
|
-
export { SALES_DOCUMENT_STATUSES, VOUCHERLIST_TYPE, countsToTotal, createSalesDocument, findDuplicateSalesDocument, findRecentSalesDocuments, getSalesDocument, invoiceExists, isPricedLine, isPursueRejection, SALES_TEXT_LIMITS, type CreatedSalesDocument, type DuplicateSearchResult, type LineItem, type PricedLineItem, type SalesDocumentDetail, type SalesDocumentKind, type SalesListEntry, type TextLineItem, } from "./sales-documents.ts";
|
|
11
|
+
export { SALES_DOCUMENT_STATUSES, SALES_TAX_TYPES, VALID_TAX_RATES, VOUCHERLIST_TYPE, countsToTotal, createSalesDocument, isVatFreeTaxType, paymentTermLabel, salesDocumentProblems, findDuplicateSalesDocument, findRecentSalesDocuments, getSalesDocument, invoiceExists, isPricedLine, isPursueRejection, SALES_TEXT_LIMITS, type AlternativeLineItem, type CreatedSalesDocument, type DuplicateSearchResult, type LineItem, type NewSalesDocument, type PaymentConditions, type PricedLineItem, type SalesLanguage, type SalesTaxType, type ShippingConditions, type TaxConditions, type SalesDocumentDetail, type SalesDocumentKind, type SalesListEntry, type TextLineItem, } from "./sales-documents.ts";
|
|
12
|
+
export { defaultPaymentCondition, getArticle, getProfile, listCountries, listPaymentConditions, listPrintLayouts, resolvePrintLayout, type Article, type Country, type CountryTaxClassification, type PaymentConditionTemplate, type PrintLayout, type Profile, } from "./settings.ts";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ export { downloadInvoicePdf, findOverdueInvoice, findOverdueInvoices, } from "./
|
|
|
5
5
|
export { DEFAULT_REMINDER_POLICY, daysPastReminderDue, earliestReminderDate, isDueImmediately, isOpenForReminder, reminderDueDate, reminderLedger, selectDueReminders, mailContentSha256, paymentReminderEffect, sendReminder, } from "./reminders.js";
|
|
6
6
|
export { MailNotSentError, defaultReminderMail, reminderSubject, } from "./reminder-mail.js";
|
|
7
7
|
export { keySegment, memoryStore } from "./store.js";
|
|
8
|
-
export { DUPLICATE_TOTAL_TOLERANCE, ITEMS_TOTAL_TOLERANCE, netTotal, round2, taxFromGross, taxFromNet, } from "./money.js";
|
|
8
|
+
export { DUPLICATE_TOTAL_TOLERANCE, ITEMS_TOTAL_TOLERANCE, lineNet, netTotal, round2, taxFromGross, taxFromNet, } from "./money.js";
|
|
9
9
|
export { REVERSE_CHARGE_TAX_RATE, isReverseChargeCategory, listPostingCategories, resolveCategory, } from "./categories.js";
|
|
10
10
|
export { createVoucher, findPurchaseInvoiceByNumber, findVendorCategoryHistory, findVouchers, uploadVoucherFile, voucherFileIds, } from "./vouchers.js";
|
|
11
|
-
export { SALES_DOCUMENT_STATUSES, VOUCHERLIST_TYPE, countsToTotal, createSalesDocument, findDuplicateSalesDocument, findRecentSalesDocuments, getSalesDocument, invoiceExists, isPricedLine, isPursueRejection, SALES_TEXT_LIMITS, } from "./sales-documents.js";
|
|
11
|
+
export { SALES_DOCUMENT_STATUSES, SALES_TAX_TYPES, VALID_TAX_RATES, VOUCHERLIST_TYPE, countsToTotal, createSalesDocument, isVatFreeTaxType, paymentTermLabel, salesDocumentProblems, findDuplicateSalesDocument, findRecentSalesDocuments, getSalesDocument, invoiceExists, isPricedLine, isPursueRejection, SALES_TEXT_LIMITS, } from "./sales-documents.js";
|
|
12
|
+
export { defaultPaymentCondition, getArticle, getProfile, listCountries, listPaymentConditions, listPrintLayouts, resolvePrintLayout, } from "./settings.js";
|
package/dist/money.d.ts
CHANGED
|
@@ -18,8 +18,12 @@ export declare function round2(value: number): number;
|
|
|
18
18
|
export interface PricedLine {
|
|
19
19
|
quantity: number;
|
|
20
20
|
netPrice: number;
|
|
21
|
+
/** Discount on the line in percent. */
|
|
22
|
+
discountPercentage?: number;
|
|
21
23
|
}
|
|
22
|
-
/** Net
|
|
24
|
+
/** Net amount of one line after its discount, unrounded. */
|
|
25
|
+
export declare const lineNet: (line: PricedLine) => number;
|
|
26
|
+
/** Net total of line items after their discounts, rounded to the cent. */
|
|
23
27
|
export declare function netTotal(lines: readonly PricedLine[]): number;
|
|
24
28
|
/** Tax contained in a gross amount. */
|
|
25
29
|
export declare function taxFromGross(gross: number, taxRatePercent: number): number;
|
package/dist/money.js
CHANGED
|
@@ -16,9 +16,11 @@ export const DUPLICATE_TOTAL_TOLERANCE = 0.02;
|
|
|
16
16
|
export function round2(value) {
|
|
17
17
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
18
18
|
}
|
|
19
|
-
/** Net
|
|
19
|
+
/** Net amount of one line after its discount, unrounded. */
|
|
20
|
+
export const lineNet = (line) => line.quantity * line.netPrice * (1 - (line.discountPercentage ?? 0) / 100);
|
|
21
|
+
/** Net total of line items after their discounts, rounded to the cent. */
|
|
20
22
|
export function netTotal(lines) {
|
|
21
|
-
return round2(lines.reduce((sum, line) => sum + line
|
|
23
|
+
return round2(lines.reduce((sum, line) => sum + lineNet(line), 0));
|
|
22
24
|
}
|
|
23
25
|
/** Tax contained in a gross amount. */
|
|
24
26
|
export function taxFromGross(gross, taxRatePercent) {
|
|
@@ -13,13 +13,32 @@ export interface PricedLineItem {
|
|
|
13
13
|
quantity: number;
|
|
14
14
|
/** e.g. "Stück", "Stunde". */
|
|
15
15
|
unit: string;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Net unit price in EUR. On a document with tax type `gross` the totals
|
|
18
|
+
* still count in net — derive it from `grossPrice` there.
|
|
19
|
+
*/
|
|
17
20
|
netPrice: number;
|
|
18
|
-
/**
|
|
21
|
+
/** Gross unit price in EUR — sent instead of `netPrice` when the document's tax type is `gross`. */
|
|
22
|
+
grossPrice?: number;
|
|
23
|
+
/** See VALID_TAX_RATES; 0 on every vat-free tax type. */
|
|
19
24
|
taxRate: number;
|
|
25
|
+
/** Discount on this line in percent, up to 2 decimals. */
|
|
26
|
+
discountPercentage?: number;
|
|
27
|
+
/**
|
|
28
|
+
* A product or service from the account's articles. The line still carries
|
|
29
|
+
* all its fields — they may deviate from the article, which stays as it is.
|
|
30
|
+
*/
|
|
31
|
+
article?: {
|
|
32
|
+
id: string;
|
|
33
|
+
type: "service" | "material";
|
|
34
|
+
};
|
|
20
35
|
/** Quotations only: printed as "Optionale Position", not part of the total. */
|
|
21
36
|
optional?: boolean;
|
|
37
|
+
/** Quotations only: alternatives offered instead of this line — printed under it, not part of the total. */
|
|
38
|
+
alternatives?: readonly AlternativeLineItem[];
|
|
22
39
|
}
|
|
40
|
+
/** An alternative to a priced line of a quotation: priced itself, never optional, without alternatives of its own. */
|
|
41
|
+
export type AlternativeLineItem = Omit<PricedLineItem, "optional" | "alternatives">;
|
|
23
42
|
/** A line without a price: a heading or a paragraph between positions. */
|
|
24
43
|
export interface TextLineItem {
|
|
25
44
|
type: "text";
|
|
@@ -35,10 +54,65 @@ export declare const countsToTotal: (item: LineItem) => item is PricedLineItem;
|
|
|
35
54
|
export declare const SALES_TEXT_LIMITS: {
|
|
36
55
|
readonly introduction: 2000;
|
|
37
56
|
readonly remark: 2000;
|
|
57
|
+
readonly deliveryTerms: 255;
|
|
38
58
|
readonly title: 25;
|
|
39
59
|
readonly lineItemName: 255;
|
|
40
60
|
readonly lineItemDescription: 2000;
|
|
41
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* The tax rates Lexware Office accepts on German sales documents (cookbook
|
|
64
|
+
* "Rechnungen"). EU destination rates for distance sales come on top, with a
|
|
65
|
+
* `taxSubType` only.
|
|
66
|
+
*/
|
|
67
|
+
export declare const VALID_TAX_RATES: readonly [0, 5, 7, 16, 19];
|
|
68
|
+
/** How the document is taxed. Every type but `net` and `gross` needs a referenced contact and 0 % on every line. */
|
|
69
|
+
export type SalesTaxType = "net" | "gross" | "vatfree" | "intraCommunitySupply" | "constructionService13b" | "externalService13b" | "thirdPartyCountryService" | "thirdPartyCountryDelivery" | "photovoltaicEquipment";
|
|
70
|
+
export declare const SALES_TAX_TYPES: readonly SalesTaxType[];
|
|
71
|
+
/** Whether a tax type charges no VAT — then every line must carry 0 %. */
|
|
72
|
+
export declare const isVatFreeTaxType: (type: SalesTaxType) => boolean;
|
|
73
|
+
export interface TaxConditions {
|
|
74
|
+
taxType: SalesTaxType;
|
|
75
|
+
/** B2C in the EU with `net` or `gross` only. */
|
|
76
|
+
taxSubType?: "distanceSales" | "electronicServices";
|
|
77
|
+
/** The printed note on a vat-free type; the account's default when absent. */
|
|
78
|
+
taxTypeNote?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface PaymentConditions {
|
|
81
|
+
/** The printed text, e.g. „Zahlbar innerhalb von 14 Tagen ohne Abzug". */
|
|
82
|
+
paymentTermLabel?: string;
|
|
83
|
+
/** Days until payment is due. */
|
|
84
|
+
paymentTermDuration?: number;
|
|
85
|
+
/** Cash discount for payment within `discountRange` days. */
|
|
86
|
+
paymentDiscountConditions?: {
|
|
87
|
+
discountPercentage: number;
|
|
88
|
+
discountRange: number;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* When the goods or the service are supplied — the date § 14 (4) No. 6 UStG
|
|
93
|
+
* asks of an invoice. `none` prints no date. The tax rates valid at
|
|
94
|
+
* `shippingDate` (a period: `shippingEndDate`) apply.
|
|
95
|
+
*/
|
|
96
|
+
export type ShippingConditions = {
|
|
97
|
+
shippingType: "service" | "delivery";
|
|
98
|
+
shippingDate: string;
|
|
99
|
+
} | {
|
|
100
|
+
shippingType: "serviceperiod" | "deliveryperiod";
|
|
101
|
+
shippingDate: string;
|
|
102
|
+
shippingEndDate: string;
|
|
103
|
+
} | {
|
|
104
|
+
shippingType: "none";
|
|
105
|
+
};
|
|
106
|
+
export type SalesLanguage = "de" | "en";
|
|
107
|
+
/**
|
|
108
|
+
* The text Lexware Office prints for a payment term, in its own wording:
|
|
109
|
+
* „Zahlbar sofort, rein netto", „Zahlbar innerhalb von 14 Tagen ohne Abzug",
|
|
110
|
+
* „10 Tage 2 % Skonto, 30 Tage netto".
|
|
111
|
+
*/
|
|
112
|
+
export declare function paymentTermLabel(days: number, discount?: {
|
|
113
|
+
percentage: number;
|
|
114
|
+
days: number;
|
|
115
|
+
}): string;
|
|
42
116
|
/** The type as the voucher list spells it. */
|
|
43
117
|
export declare const VOUCHERLIST_TYPE: Record<SalesDocumentKind, string>;
|
|
44
118
|
/**
|
|
@@ -51,35 +125,63 @@ export interface CreatedSalesDocument {
|
|
|
51
125
|
resourceUri?: string;
|
|
52
126
|
url: string;
|
|
53
127
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
* then no longer be deleted like a draft. `introduction` and `remark` replace
|
|
57
|
-
* the default texts of the account's settings — left out, the defaults stay.
|
|
58
|
-
* Only quotations know optional lines; on other kinds this throws instead of
|
|
59
|
-
* the API dropping the flag and counting the line.
|
|
60
|
-
*
|
|
61
|
-
* `precedingSalesVoucherId` links the document to its predecessor (a pursued
|
|
62
|
-
* quotation becomes accepted) — only as a query parameter; in the body the API
|
|
63
|
-
* drops it silently. The content is not copied: this payload alone decides
|
|
64
|
-
* the new document. An invalid pursue answers 406, see isPursueRejection.
|
|
65
|
-
*/
|
|
66
|
-
export declare function createSalesDocument(client: LexwareClient, options: {
|
|
128
|
+
/** What a sales document is created with. Everything optional falls back to Lexware Office's defaults. */
|
|
129
|
+
export interface NewSalesDocument {
|
|
67
130
|
kind: SalesDocumentKind;
|
|
68
131
|
contactId: string;
|
|
69
132
|
items: readonly LineItem[];
|
|
133
|
+
/** yyyy-MM-dd or a timestamp. Default: now. */
|
|
134
|
+
voucherDate?: string;
|
|
70
135
|
/** Replaces the printed heading; Lexware's default when absent. Max. 25 characters. */
|
|
71
136
|
title?: string;
|
|
72
137
|
/** Quotations: valid until, yyyy-MM-dd or a timestamp. */
|
|
73
138
|
expirationDate?: string;
|
|
74
139
|
finalize?: boolean;
|
|
75
|
-
/** Total discount in EUR
|
|
140
|
+
/** Total discount in EUR. Not together with `totalDiscountPercentage`. */
|
|
76
141
|
totalDiscountAbsolute?: number;
|
|
142
|
+
/** Total discount in percent of the net (or, on `gross`, the gross) total. Without either, a contact's default discount applies. */
|
|
143
|
+
totalDiscountPercentage?: number;
|
|
144
|
+
/** Default: `{ taxType: "net" }`. */
|
|
145
|
+
taxConditions?: TaxConditions;
|
|
146
|
+
/** Without them, Lexware takes the contact's payment term, else the account's default. */
|
|
147
|
+
paymentConditions?: PaymentConditions;
|
|
148
|
+
/**
|
|
149
|
+
* Order confirmations and invoices only — quotations have none. Default:
|
|
150
|
+
* delivery on the voucher date, which is what Lexware prints then.
|
|
151
|
+
*/
|
|
152
|
+
shippingConditions?: ShippingConditions;
|
|
153
|
+
/** Print language and language of the default texts. Default: de. */
|
|
154
|
+
language?: SalesLanguage;
|
|
155
|
+
/** A print layout from listPrintLayouts; the account's default when absent. */
|
|
156
|
+
printLayoutId?: string;
|
|
157
|
+
/** Order confirmations only. Max. 255 characters; the contact's or account's default when absent. */
|
|
158
|
+
deliveryTerms?: string;
|
|
159
|
+
/** Invoices to an XRechnung contact: the Leitweg-ID, overriding the contact's. An empty string makes a standard invoice. */
|
|
160
|
+
xRechnung?: {
|
|
161
|
+
buyerReference: string;
|
|
162
|
+
};
|
|
77
163
|
precedingSalesVoucherId?: string;
|
|
78
164
|
/** Opening text; Markdown. Max. 2000 characters. */
|
|
79
165
|
introduction?: string;
|
|
80
166
|
/** Closing text; Markdown. Max. 2000 characters. */
|
|
81
167
|
remark?: string;
|
|
82
|
-
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Creates a sales document. `finalize` gives it a number right away — it can
|
|
171
|
+
* then no longer be deleted like a draft. Texts, payment conditions, delivery
|
|
172
|
+
* terms, print layout and total discount left out take the account's (or the
|
|
173
|
+
* contact's) defaults. Combinations the API documents for another kind only —
|
|
174
|
+
* optional lines outside a quotation, shipping conditions on one, … — throw
|
|
175
|
+
* instead of reaching the API, which would drop or reject them.
|
|
176
|
+
*
|
|
177
|
+
* `precedingSalesVoucherId` links the document to its predecessor (a pursued
|
|
178
|
+
* quotation becomes accepted) — only as a query parameter; in the body the API
|
|
179
|
+
* drops it silently. The content is not copied: this payload alone decides
|
|
180
|
+
* the new document. An invalid pursue answers 406, see isPursueRejection.
|
|
181
|
+
*/
|
|
182
|
+
export declare function createSalesDocument(client: LexwareClient, options: NewSalesDocument, signal?: AbortSignal): Promise<CreatedSalesDocument>;
|
|
183
|
+
/** The checks createSalesDocument runs before sending — exported so a caller can run them before an approval. */
|
|
184
|
+
export declare function salesDocumentProblems(options: NewSalesDocument): string[];
|
|
83
185
|
/** Whether a 406 rejects the pursue, not the payload — then a retry without the link makes sense. */
|
|
84
186
|
export declare function isPursueRejection(error: unknown): boolean;
|
|
85
187
|
export interface SalesDocumentDetail {
|
package/dist/sales-documents.js
CHANGED
|
@@ -3,12 +3,42 @@
|
|
|
3
3
|
* one payload shape; only the endpoint and a few optional fields differ.
|
|
4
4
|
*/
|
|
5
5
|
import { LexwareApiError, query } from "./client.js";
|
|
6
|
-
import { DUPLICATE_TOTAL_TOLERANCE, round2 } from "./money.js";
|
|
6
|
+
import { DUPLICATE_TOTAL_TOLERANCE, netTotal, round2 } from "./money.js";
|
|
7
7
|
export const isPricedLine = (item) => item.type !== "text";
|
|
8
8
|
/** Whether a line adds to the document's total: priced and not optional. */
|
|
9
9
|
export const countsToTotal = (item) => isPricedLine(item) && !item.optional;
|
|
10
10
|
/** Lexware's limits for the texts of a sales document (FAQ "Texts in Sales Vouchers"). */
|
|
11
|
-
export const SALES_TEXT_LIMITS = { introduction: 2000, remark: 2000, title: 25, lineItemName: 255, lineItemDescription: 2000 };
|
|
11
|
+
export const SALES_TEXT_LIMITS = { introduction: 2000, remark: 2000, deliveryTerms: 255, title: 25, lineItemName: 255, lineItemDescription: 2000 };
|
|
12
|
+
/**
|
|
13
|
+
* The tax rates Lexware Office accepts on German sales documents (cookbook
|
|
14
|
+
* "Rechnungen"). EU destination rates for distance sales come on top, with a
|
|
15
|
+
* `taxSubType` only.
|
|
16
|
+
*/
|
|
17
|
+
export const VALID_TAX_RATES = [0, 5, 7, 16, 19];
|
|
18
|
+
export const SALES_TAX_TYPES = [
|
|
19
|
+
"net",
|
|
20
|
+
"gross",
|
|
21
|
+
"vatfree",
|
|
22
|
+
"intraCommunitySupply",
|
|
23
|
+
"constructionService13b",
|
|
24
|
+
"externalService13b",
|
|
25
|
+
"thirdPartyCountryService",
|
|
26
|
+
"thirdPartyCountryDelivery",
|
|
27
|
+
"photovoltaicEquipment",
|
|
28
|
+
];
|
|
29
|
+
/** Whether a tax type charges no VAT — then every line must carry 0 %. */
|
|
30
|
+
export const isVatFreeTaxType = (type) => type !== "net" && type !== "gross";
|
|
31
|
+
/**
|
|
32
|
+
* The text Lexware Office prints for a payment term, in its own wording:
|
|
33
|
+
* „Zahlbar sofort, rein netto", „Zahlbar innerhalb von 14 Tagen ohne Abzug",
|
|
34
|
+
* „10 Tage 2 % Skonto, 30 Tage netto".
|
|
35
|
+
*/
|
|
36
|
+
export function paymentTermLabel(days, discount) {
|
|
37
|
+
if (discount) {
|
|
38
|
+
return `${discount.days} Tage ${discount.percentage.toLocaleString("de-DE", { maximumFractionDigits: 2 })} % Skonto, ${days} Tage netto`;
|
|
39
|
+
}
|
|
40
|
+
return days === 0 ? "Zahlbar sofort, rein netto" : `Zahlbar innerhalb von ${days} ${days === 1 ? "Tag" : "Tagen"} ohne Abzug`;
|
|
41
|
+
}
|
|
12
42
|
const ENDPOINTS = {
|
|
13
43
|
quotation: "/quotations",
|
|
14
44
|
"order-confirmation": "/order-confirmations",
|
|
@@ -31,10 +61,11 @@ export const SALES_DOCUMENT_STATUSES = {
|
|
|
31
61
|
};
|
|
32
62
|
/**
|
|
33
63
|
* Creates a sales document. `finalize` gives it a number right away — it can
|
|
34
|
-
* then no longer be deleted like a draft.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
64
|
+
* then no longer be deleted like a draft. Texts, payment conditions, delivery
|
|
65
|
+
* terms, print layout and total discount left out take the account's (or the
|
|
66
|
+
* contact's) defaults. Combinations the API documents for another kind only —
|
|
67
|
+
* optional lines outside a quotation, shipping conditions on one, … — throw
|
|
68
|
+
* instead of reaching the API, which would drop or reject them.
|
|
38
69
|
*
|
|
39
70
|
* `precedingSalesVoucherId` links the document to its predecessor (a pursued
|
|
40
71
|
* quotation becomes accepted) — only as a query parameter; in the body the API
|
|
@@ -42,18 +73,27 @@ export const SALES_DOCUMENT_STATUSES = {
|
|
|
42
73
|
* the new document. An invalid pursue answers 406, see isPursueRejection.
|
|
43
74
|
*/
|
|
44
75
|
export async function createSalesDocument(client, options, signal) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
const
|
|
49
|
-
const
|
|
76
|
+
assertValidSalesDocument(options);
|
|
77
|
+
const voucherDate = options.voucherDate ? toIsoDateTime(options.voucherDate) : new Date().toISOString().replace("Z", "+00:00");
|
|
78
|
+
const taxConditions = options.taxConditions ?? { taxType: "net" };
|
|
79
|
+
const gross = taxConditions.taxType === "gross";
|
|
80
|
+
const totalPrice = {
|
|
81
|
+
currency: "EUR",
|
|
82
|
+
...((options.totalDiscountAbsolute ?? 0) > 0 ? { totalDiscountAbsolute: round2(options.totalDiscountAbsolute) } : {}),
|
|
83
|
+
...((options.totalDiscountPercentage ?? 0) > 0 ? { totalDiscountPercentage: round2(options.totalDiscountPercentage) } : {}),
|
|
84
|
+
};
|
|
50
85
|
const payload = {
|
|
51
86
|
voucherDate,
|
|
52
87
|
address: { contactId: options.contactId },
|
|
53
|
-
lineItems: options.items.map(toApiLine),
|
|
54
|
-
totalPrice
|
|
55
|
-
taxConditions
|
|
56
|
-
|
|
88
|
+
lineItems: options.items.map((item) => toApiLine(item, gross)),
|
|
89
|
+
totalPrice,
|
|
90
|
+
taxConditions,
|
|
91
|
+
...(options.kind === "quotation" ? {} : { shippingConditions: toApiShipping(options.shippingConditions, voucherDate) }),
|
|
92
|
+
...(options.paymentConditions ? { paymentConditions: options.paymentConditions } : {}),
|
|
93
|
+
...(options.language ? { language: options.language } : {}),
|
|
94
|
+
...(options.printLayoutId ? { printLayoutId: options.printLayoutId } : {}),
|
|
95
|
+
...(options.deliveryTerms ? { deliveryTerms: options.deliveryTerms } : {}),
|
|
96
|
+
...(options.xRechnung ? { xRechnung: options.xRechnung } : {}),
|
|
57
97
|
...(options.title ? { title: options.title } : {}),
|
|
58
98
|
...(options.expirationDate ? { expirationDate: toIsoDateTime(options.expirationDate) } : {}),
|
|
59
99
|
...(options.introduction ? { introduction: options.introduction } : {}),
|
|
@@ -67,18 +107,95 @@ export async function createSalesDocument(client, options, signal) {
|
|
|
67
107
|
});
|
|
68
108
|
return { id: result.id, resourceUri: result.resourceUri, url: client.voucherUrl(result.id) };
|
|
69
109
|
}
|
|
70
|
-
|
|
110
|
+
/** The checks createSalesDocument runs before sending — exported so a caller can run them before an approval. */
|
|
111
|
+
export function salesDocumentProblems(options) {
|
|
112
|
+
const problems = [];
|
|
113
|
+
const { kind } = options;
|
|
114
|
+
const priced = options.items.filter(isPricedLine);
|
|
115
|
+
const lines = priced.flatMap((item) => [item, ...(item.alternatives ?? [])]);
|
|
116
|
+
const taxType = options.taxConditions?.taxType ?? "net";
|
|
117
|
+
if (kind !== "quotation" && priced.some((item) => item.optional))
|
|
118
|
+
problems.push(`Optional line items exist only on quotations, not on ${kind}`);
|
|
119
|
+
if (kind !== "quotation" && priced.some((item) => (item.alternatives?.length ?? 0) > 0))
|
|
120
|
+
problems.push(`Alternative line items exist only on quotations, not on ${kind}`);
|
|
121
|
+
if (priced.some((item) => item.optional && (item.alternatives?.length ?? 0) > 0))
|
|
122
|
+
problems.push("An optional line item cannot have alternatives");
|
|
123
|
+
if (kind === "quotation" && options.shippingConditions)
|
|
124
|
+
problems.push("Quotations have no shipping conditions");
|
|
125
|
+
if (kind !== "order-confirmation" && options.deliveryTerms)
|
|
126
|
+
problems.push(`Delivery terms exist only on order confirmations, not on ${kind}`);
|
|
127
|
+
if (kind !== "invoice" && options.xRechnung)
|
|
128
|
+
problems.push(`XRechnung properties exist only on invoices, not on ${kind}`);
|
|
129
|
+
if (kind !== "quotation" && options.expirationDate)
|
|
130
|
+
problems.push(`An expiration date exists only on quotations, not on ${kind}`);
|
|
131
|
+
if ((options.deliveryTerms?.length ?? 0) > SALES_TEXT_LIMITS.deliveryTerms)
|
|
132
|
+
problems.push(`Delivery terms longer than ${SALES_TEXT_LIMITS.deliveryTerms} characters`);
|
|
133
|
+
if ((options.totalDiscountAbsolute ?? 0) > 0 && (options.totalDiscountPercentage ?? 0) > 0)
|
|
134
|
+
problems.push("Either an absolute or a percentage total discount, not both");
|
|
135
|
+
if ((options.totalDiscountPercentage ?? 0) > 100)
|
|
136
|
+
problems.push("A total discount above 100 %");
|
|
137
|
+
if (isVatFreeTaxType(taxType) && lines.some((item) => item.taxRate !== 0))
|
|
138
|
+
problems.push(`Tax type ${taxType} needs 0 % on every line`);
|
|
139
|
+
if (options.taxConditions?.taxSubType && isVatFreeTaxType(taxType))
|
|
140
|
+
problems.push("A tax subtype goes with net or gross only");
|
|
141
|
+
if (!options.taxConditions?.taxSubType && lines.some((item) => !VALID_TAX_RATES.includes(item.taxRate))) {
|
|
142
|
+
problems.push(`Tax rates other than ${VALID_TAX_RATES.join(", ")} % need a tax subtype (EU distance sales)`);
|
|
143
|
+
}
|
|
144
|
+
if (taxType === "gross" && lines.some((item) => item.grossPrice === undefined))
|
|
145
|
+
problems.push("Tax type gross needs a gross price on every priced line");
|
|
146
|
+
if (lines.some((item) => (item.discountPercentage ?? 0) < 0 || (item.discountPercentage ?? 0) > 100))
|
|
147
|
+
problems.push("A line discount outside 0–100 %");
|
|
148
|
+
const shipping = options.shippingConditions;
|
|
149
|
+
if (shipping && shipping.shippingType !== "none") {
|
|
150
|
+
if (!isDate(shipping.shippingDate))
|
|
151
|
+
problems.push(`Shipping date ${shipping.shippingDate} is no date`);
|
|
152
|
+
if ("shippingEndDate" in shipping) {
|
|
153
|
+
if (!isDate(shipping.shippingEndDate))
|
|
154
|
+
problems.push(`Shipping end date ${shipping.shippingEndDate} is no date`);
|
|
155
|
+
else if (dayOf(shipping.shippingEndDate) < dayOf(shipping.shippingDate))
|
|
156
|
+
problems.push("The shipping period ends before it starts");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const payment = options.paymentConditions;
|
|
160
|
+
if (payment?.paymentDiscountConditions && payment.paymentTermDuration !== undefined && payment.paymentDiscountConditions.discountRange > payment.paymentTermDuration) {
|
|
161
|
+
problems.push("The cash discount runs longer than the payment term");
|
|
162
|
+
}
|
|
163
|
+
return problems;
|
|
164
|
+
}
|
|
165
|
+
function assertValidSalesDocument(options) {
|
|
166
|
+
const problems = salesDocumentProblems(options);
|
|
167
|
+
if (problems.length > 0)
|
|
168
|
+
throw new Error(problems.join("; "));
|
|
169
|
+
}
|
|
170
|
+
const isDate = (value) => !Number.isNaN(new Date(/^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T12:00:00Z` : value).getTime());
|
|
171
|
+
const dayOf = (value) => value.slice(0, 10);
|
|
172
|
+
function toApiShipping(shipping, voucherDate) {
|
|
173
|
+
if (!shipping)
|
|
174
|
+
return { shippingType: "delivery", shippingDate: voucherDate };
|
|
175
|
+
if (shipping.shippingType === "none")
|
|
176
|
+
return { shippingType: "none" };
|
|
177
|
+
return {
|
|
178
|
+
shippingType: shipping.shippingType,
|
|
179
|
+
shippingDate: toIsoDateTime(shipping.shippingDate),
|
|
180
|
+
...("shippingEndDate" in shipping ? { shippingEndDate: toIsoDateTime(shipping.shippingEndDate) } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function toApiLine(item, gross) {
|
|
71
184
|
const description = item.description ? { description: item.description } : {};
|
|
72
185
|
if (!isPricedLine(item))
|
|
73
186
|
return { type: "text", name: item.name, ...description };
|
|
74
187
|
return {
|
|
75
|
-
type: "custom",
|
|
188
|
+
...(item.article ? { id: item.article.id, type: item.article.type } : { type: "custom" }),
|
|
76
189
|
name: item.name,
|
|
77
190
|
...description,
|
|
78
191
|
quantity: item.quantity,
|
|
79
192
|
unitName: item.unit,
|
|
80
|
-
unitPrice:
|
|
193
|
+
unitPrice: gross
|
|
194
|
+
? { currency: "EUR", grossAmount: item.grossPrice, taxRatePercentage: item.taxRate }
|
|
195
|
+
: { currency: "EUR", netAmount: item.netPrice, taxRatePercentage: item.taxRate },
|
|
196
|
+
...((item.discountPercentage ?? 0) > 0 ? { discountPercentage: item.discountPercentage } : {}),
|
|
81
197
|
...(item.optional ? { optional: true } : {}),
|
|
198
|
+
...(item.alternatives?.length ? { subItems: item.alternatives.map((alternative) => ({ ...toApiLine(alternative, gross), alternative: true })) } : {}),
|
|
82
199
|
};
|
|
83
200
|
}
|
|
84
201
|
/** Whether a 406 rejects the pursue, not the payload — then a retry without the link makes sense. */
|
|
@@ -105,6 +222,7 @@ export async function getSalesDocument(client, kind, id, signal) {
|
|
|
105
222
|
unit: item.unitName ?? "Stück",
|
|
106
223
|
netPrice: item.unitPrice?.netAmount ?? 0,
|
|
107
224
|
taxRate: item.unitPrice?.taxRatePercentage ?? 19,
|
|
225
|
+
...(item.discountPercentage ? { discountPercentage: item.discountPercentage } : {}),
|
|
108
226
|
...(item.optional ? { optional: true } : {}),
|
|
109
227
|
}));
|
|
110
228
|
const items = priced.filter((item) => !item.optional);
|
|
@@ -115,7 +233,7 @@ export async function getSalesDocument(client, kind, id, signal) {
|
|
|
115
233
|
voucherStatus: raw.voucherStatus,
|
|
116
234
|
contactId: raw.address?.contactId,
|
|
117
235
|
contactName: raw.address?.name,
|
|
118
|
-
lineItemsNet: items.length === 0 ? Number.NaN :
|
|
236
|
+
lineItemsNet: items.length === 0 ? Number.NaN : netTotal(items),
|
|
119
237
|
lineItems: items,
|
|
120
238
|
optionalItems: priced.filter((item) => item.optional),
|
|
121
239
|
hasOptionalOrAlternative: lines.some((item) => item.optional === true || (item.subItems?.length ?? 0) > 0),
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account settings a sales document refers to: print layouts, payment
|
|
3
|
+
* conditions, the profile's tax setup, countries, articles. Read-only.
|
|
4
|
+
*/
|
|
5
|
+
import { type LexwareClient } from "./client.ts";
|
|
6
|
+
export interface PrintLayout {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
default: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function listPrintLayouts(client: LexwareClient, signal?: AbortSignal): Promise<PrintLayout[]>;
|
|
12
|
+
/** A print layout by id or by name (case-insensitive); null if none or several match. */
|
|
13
|
+
export declare function resolvePrintLayout(client: LexwareClient, nameOrId: string, signal?: AbortSignal): Promise<PrintLayout | null>;
|
|
14
|
+
export interface PaymentConditionTemplate {
|
|
15
|
+
id: string;
|
|
16
|
+
/** As configured, with placeholders such as {paymentRange}. */
|
|
17
|
+
paymentTermLabelTemplate: string;
|
|
18
|
+
paymentTermDuration?: number;
|
|
19
|
+
paymentDiscountConditions?: {
|
|
20
|
+
discountPercentage: number;
|
|
21
|
+
discountRange: number;
|
|
22
|
+
};
|
|
23
|
+
organizationDefault: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function listPaymentConditions(client: LexwareClient, signal?: AbortSignal): Promise<PaymentConditionTemplate[]>;
|
|
26
|
+
/**
|
|
27
|
+
* The account's default payment condition — what a document without payment
|
|
28
|
+
* conditions gets unless its contact has its own (which the API does not
|
|
29
|
+
* expose). The label with its placeholders filled in.
|
|
30
|
+
*/
|
|
31
|
+
export declare function defaultPaymentCondition(client: LexwareClient, signal?: AbortSignal): Promise<{
|
|
32
|
+
label: string;
|
|
33
|
+
duration?: number;
|
|
34
|
+
discount?: {
|
|
35
|
+
percentage: number;
|
|
36
|
+
days: number;
|
|
37
|
+
};
|
|
38
|
+
} | null>;
|
|
39
|
+
export interface Profile {
|
|
40
|
+
organizationId: string;
|
|
41
|
+
companyName?: string;
|
|
42
|
+
/** The account's tax type: net, gross, or vatfree. */
|
|
43
|
+
taxType?: "net" | "gross" | "vatfree";
|
|
44
|
+
/** Kleinunternehmer according to § 19 UStG. */
|
|
45
|
+
smallBusiness?: boolean;
|
|
46
|
+
distanceSalesPrinciple?: "ORIGIN" | "DESTINATION";
|
|
47
|
+
}
|
|
48
|
+
export declare function getProfile(client: LexwareClient, signal?: AbortSignal): Promise<Profile>;
|
|
49
|
+
export type CountryTaxClassification = "de" | "intraCommunity" | "thirdPartyCountry";
|
|
50
|
+
export interface Country {
|
|
51
|
+
countryCode: string;
|
|
52
|
+
countryNameDE?: string;
|
|
53
|
+
countryNameEN?: string;
|
|
54
|
+
taxClassification: CountryTaxClassification;
|
|
55
|
+
}
|
|
56
|
+
export declare function listCountries(client: LexwareClient, signal?: AbortSignal): Promise<Country[]>;
|
|
57
|
+
export interface Article {
|
|
58
|
+
id: string;
|
|
59
|
+
title: string;
|
|
60
|
+
/** As a line item spells it. */
|
|
61
|
+
lineType: "service" | "material";
|
|
62
|
+
articleNumber?: string;
|
|
63
|
+
unitName?: string;
|
|
64
|
+
netPrice?: number;
|
|
65
|
+
grossPrice?: number;
|
|
66
|
+
taxRate?: number;
|
|
67
|
+
}
|
|
68
|
+
export declare function getArticle(client: LexwareClient, id: string, signal?: AbortSignal): Promise<Article>;
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account settings a sales document refers to: print layouts, payment
|
|
3
|
+
* conditions, the profile's tax setup, countries, articles. Read-only.
|
|
4
|
+
*/
|
|
5
|
+
import {} from "./client.js";
|
|
6
|
+
export async function listPrintLayouts(client, signal) {
|
|
7
|
+
return (await client.request("/print-layouts", { signal })) ?? [];
|
|
8
|
+
}
|
|
9
|
+
/** A print layout by id or by name (case-insensitive); null if none or several match. */
|
|
10
|
+
export async function resolvePrintLayout(client, nameOrId, signal) {
|
|
11
|
+
const needle = nameOrId.trim().toLowerCase();
|
|
12
|
+
const layouts = await listPrintLayouts(client, signal);
|
|
13
|
+
const byId = layouts.find((layout) => layout.id.toLowerCase() === needle);
|
|
14
|
+
if (byId)
|
|
15
|
+
return byId;
|
|
16
|
+
const byName = layouts.filter((layout) => layout.name.trim().toLowerCase() === needle);
|
|
17
|
+
return byName.length === 1 ? byName[0] : null;
|
|
18
|
+
}
|
|
19
|
+
export async function listPaymentConditions(client, signal) {
|
|
20
|
+
return (await client.request("/payment-conditions", { signal })) ?? [];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The account's default payment condition — what a document without payment
|
|
24
|
+
* conditions gets unless its contact has its own (which the API does not
|
|
25
|
+
* expose). The label with its placeholders filled in.
|
|
26
|
+
*/
|
|
27
|
+
export async function defaultPaymentCondition(client, signal) {
|
|
28
|
+
const found = (await listPaymentConditions(client, signal)).find((condition) => condition.organizationDefault);
|
|
29
|
+
if (!found)
|
|
30
|
+
return null;
|
|
31
|
+
const discount = found.paymentDiscountConditions;
|
|
32
|
+
const label = found.paymentTermLabelTemplate
|
|
33
|
+
.replace(/\{paymentRange\}/g, String(found.paymentTermDuration ?? ""))
|
|
34
|
+
.replace(/\{discountRange\}/g, String(discount?.discountRange ?? ""))
|
|
35
|
+
.replace(/\{discount\}/g, discount ? `${discount.discountPercentage.toLocaleString("de-DE", { maximumFractionDigits: 2 })} %` : "");
|
|
36
|
+
return {
|
|
37
|
+
label,
|
|
38
|
+
...(found.paymentTermDuration !== undefined ? { duration: found.paymentTermDuration } : {}),
|
|
39
|
+
...(discount ? { discount: { percentage: discount.discountPercentage, days: discount.discountRange } } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function getProfile(client, signal) {
|
|
43
|
+
return client.request("/profile", { signal });
|
|
44
|
+
}
|
|
45
|
+
export async function listCountries(client, signal) {
|
|
46
|
+
return (await client.request("/countries", { signal })) ?? [];
|
|
47
|
+
}
|
|
48
|
+
export async function getArticle(client, id, signal) {
|
|
49
|
+
const raw = await client.request(`/articles/${encodeURIComponent(id)}`, { signal });
|
|
50
|
+
return {
|
|
51
|
+
id: raw.id,
|
|
52
|
+
title: raw.title ?? "",
|
|
53
|
+
lineType: raw.type === "PRODUCT" ? "material" : "service",
|
|
54
|
+
...(raw.articleNumber ? { articleNumber: raw.articleNumber } : {}),
|
|
55
|
+
...(raw.unitName ? { unitName: raw.unitName } : {}),
|
|
56
|
+
...(raw.price?.netPrice !== undefined ? { netPrice: raw.price.netPrice } : {}),
|
|
57
|
+
...(raw.price?.grossPrice !== undefined ? { grossPrice: raw.price.grossPrice } : {}),
|
|
58
|
+
...(raw.price?.taxRate !== undefined ? { taxRate: raw.price.taxRate } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
package/package.json
CHANGED