@voyantjs/finance 0.3.0 → 0.4.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.
Files changed (43) hide show
  1. package/dist/index.d.ts +18 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +25 -5
  4. package/dist/routes-documents.d.ts +159 -0
  5. package/dist/routes-documents.d.ts.map +1 -0
  6. package/dist/routes-documents.js +35 -0
  7. package/dist/routes-public.d.ts +517 -0
  8. package/dist/routes-public.d.ts.map +1 -0
  9. package/dist/routes-public.js +60 -0
  10. package/dist/routes-settlement.d.ts +63 -0
  11. package/dist/routes-settlement.d.ts.map +1 -0
  12. package/dist/routes-settlement.js +18 -0
  13. package/dist/routes-shared.d.ts +12 -0
  14. package/dist/routes-shared.d.ts.map +1 -0
  15. package/dist/routes-shared.js +3 -0
  16. package/dist/routes.d.ts +207 -161
  17. package/dist/routes.d.ts.map +1 -1
  18. package/dist/routes.js +40 -1
  19. package/dist/schema.d.ts +17 -17
  20. package/dist/service-documents.d.ts +67 -0
  21. package/dist/service-documents.d.ts.map +1 -0
  22. package/dist/service-documents.js +226 -0
  23. package/dist/service-public.d.ts +251 -0
  24. package/dist/service-public.d.ts.map +1 -0
  25. package/dist/service-public.js +418 -0
  26. package/dist/service-settlement.d.ts +36 -0
  27. package/dist/service-settlement.d.ts.map +1 -0
  28. package/dist/service-settlement.js +172 -0
  29. package/dist/service.d.ts +175 -181
  30. package/dist/service.d.ts.map +1 -1
  31. package/dist/service.js +54 -67
  32. package/dist/validation-billing.d.ts +119 -9
  33. package/dist/validation-billing.d.ts.map +1 -1
  34. package/dist/validation-billing.js +54 -1
  35. package/dist/validation-payments.d.ts +83 -83
  36. package/dist/validation-public.d.ts +443 -0
  37. package/dist/validation-public.d.ts.map +1 -0
  38. package/dist/validation-public.js +183 -0
  39. package/dist/validation-shared.d.ts +24 -24
  40. package/dist/validation.d.ts +1 -0
  41. package/dist/validation.d.ts.map +1 -1
  42. package/dist/validation.js +1 -0
  43. package/package.json +15 -5
@@ -0,0 +1,36 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import type { Invoice as FinanceInvoice, InvoiceExternalRef } from "./schema.js";
3
+ import type { PolledInvoiceSettlementResult, PollInvoiceSettlementInput } from "./validation.js";
4
+ type SettlementInvoice = FinanceInvoice;
5
+ type SettlementExternalRef = InvoiceExternalRef;
6
+ export interface InvoiceSettlementPollerContext {
7
+ db: PostgresJsDatabase;
8
+ invoice: SettlementInvoice;
9
+ externalRef: SettlementExternalRef;
10
+ bindings: Record<string, unknown>;
11
+ }
12
+ export interface InvoiceSettlementPollerResult {
13
+ externalId?: string | null;
14
+ externalNumber?: string | null;
15
+ externalUrl?: string | null;
16
+ status?: string | null;
17
+ paidAmountCents?: number | null;
18
+ unpaidAmountCents?: number | null;
19
+ syncedAt?: string | Date | null;
20
+ settledAt?: string | Date | null;
21
+ referenceNumber?: string | null;
22
+ syncError?: string | null;
23
+ metadata?: Record<string, unknown> | null;
24
+ }
25
+ export type InvoiceSettlementPoller = (context: InvoiceSettlementPollerContext) => Promise<InvoiceSettlementPollerResult>;
26
+ export interface FinanceSettlementRuntimeOptions {
27
+ bindings?: Record<string, unknown>;
28
+ invoiceSettlementPollers?: Record<string, InvoiceSettlementPoller>;
29
+ }
30
+ export declare const financeSettlementService: {
31
+ pollInvoiceSettlement(db: PostgresJsDatabase, invoiceId: string, input: PollInvoiceSettlementInput, runtime?: FinanceSettlementRuntimeOptions): Promise<PolledInvoiceSettlementResult | {
32
+ status: "not_found";
33
+ }>;
34
+ };
35
+ export {};
36
+ //# sourceMappingURL=service-settlement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-settlement.d.ts","sourceRoot":"","sources":["../src/service-settlement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,KAAK,EAAE,OAAO,IAAI,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAEhF,OAAO,KAAK,EAAE,6BAA6B,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAA;AAEhG,KAAK,iBAAiB,GAAG,cAAc,CAAA;AACvC,KAAK,qBAAqB,GAAG,kBAAkB,CAAA;AAE/C,MAAM,WAAW,8BAA8B;IAC7C,EAAE,EAAE,kBAAkB,CAAA;IACtB,OAAO,EAAE,iBAAiB,CAAA;IAC1B,WAAW,EAAE,qBAAqB,CAAA;IAClC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,6BAA6B;IAC5C,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAChC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC1C;AAED,MAAM,MAAM,uBAAuB,GAAG,CACpC,OAAO,EAAE,8BAA8B,KACpC,OAAO,CAAC,6BAA6B,CAAC,CAAA;AAE3C,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,wBAAwB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAA;CACnE;AAyED,eAAO,MAAM,wBAAwB;8BAE7B,kBAAkB,aACX,MAAM,SACV,0BAA0B,YACxB,+BAA+B,GACvC,OAAO,CAAC,6BAA6B,GAAG;QAAE,MAAM,EAAE,WAAW,CAAA;KAAE,CAAC;CAwIpE,CAAA"}
@@ -0,0 +1,172 @@
1
+ import { financeService } from "./service.js";
2
+ function coerceRecord(value) {
3
+ return value && typeof value === "object" && !Array.isArray(value)
4
+ ? value
5
+ : null;
6
+ }
7
+ function toIsoString(value) {
8
+ if (!value)
9
+ return null;
10
+ if (value instanceof Date)
11
+ return value.toISOString();
12
+ const parsed = new Date(value);
13
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
14
+ }
15
+ function normalizeMoney(value) {
16
+ if (typeof value !== "number" || Number.isNaN(value))
17
+ return null;
18
+ return Math.round(value);
19
+ }
20
+ function resolveReferenceNumber(input, pollResult, externalRef, invoice) {
21
+ return (input.referenceNumber ??
22
+ pollResult.referenceNumber ??
23
+ pollResult.externalNumber ??
24
+ pollResult.externalId ??
25
+ externalRef.externalNumber ??
26
+ externalRef.externalId ??
27
+ invoice.invoiceNumber);
28
+ }
29
+ function resolvePaymentDate(input, pollResult) {
30
+ return (input.paymentDate ??
31
+ toIsoString(pollResult.settledAt) ??
32
+ toIsoString(pollResult.syncedAt) ??
33
+ new Date().toISOString());
34
+ }
35
+ async function synchronizeExternalRef(db, invoiceId, externalRef, pollResult) {
36
+ const syncedAt = toIsoString(pollResult.syncedAt) ?? new Date().toISOString();
37
+ const row = await financeService.registerInvoiceExternalRef(db, invoiceId, {
38
+ provider: externalRef.provider,
39
+ externalId: pollResult.externalId ?? externalRef.externalId ?? null,
40
+ externalNumber: pollResult.externalNumber ?? externalRef.externalNumber ?? null,
41
+ externalUrl: pollResult.externalUrl ?? externalRef.externalUrl ?? null,
42
+ status: pollResult.status ?? externalRef.status ?? null,
43
+ metadata: pollResult.metadata ?? coerceRecord(externalRef.metadata) ?? null,
44
+ syncedAt,
45
+ syncError: pollResult.syncError ?? null,
46
+ });
47
+ return {
48
+ row,
49
+ syncedAt,
50
+ };
51
+ }
52
+ export const financeSettlementService = {
53
+ async pollInvoiceSettlement(db, invoiceId, input, runtime = {}) {
54
+ let invoice = await financeService.getInvoiceById(db, invoiceId);
55
+ if (!invoice) {
56
+ return { status: "not_found" };
57
+ }
58
+ const externalRefs = await financeService.listInvoiceExternalRefs(db, invoiceId);
59
+ const refsToPoll = input.provider
60
+ ? externalRefs.filter((externalRef) => externalRef.provider === input.provider)
61
+ : externalRefs;
62
+ const results = [];
63
+ for (const externalRef of refsToPoll) {
64
+ const poller = runtime.invoiceSettlementPollers?.[externalRef.provider];
65
+ if (!poller) {
66
+ const synced = await synchronizeExternalRef(db, invoice.id, externalRef, {
67
+ syncError: "No settlement poller configured",
68
+ });
69
+ results.push({
70
+ provider: externalRef.provider,
71
+ externalRefId: synced.row?.id ?? externalRef.id,
72
+ externalId: synced.row?.externalId ?? externalRef.externalId ?? null,
73
+ externalNumber: synced.row?.externalNumber ?? externalRef.externalNumber ?? null,
74
+ externalUrl: synced.row?.externalUrl ?? externalRef.externalUrl ?? null,
75
+ status: synced.row?.status ?? externalRef.status ?? null,
76
+ paidAmountCents: null,
77
+ unpaidAmountCents: null,
78
+ syncedAt: toIsoString(synced.row?.syncedAt) ?? synced.syncedAt,
79
+ settledAt: null,
80
+ createdPaymentId: null,
81
+ newlyAppliedAmountCents: 0,
82
+ syncError: synced.row?.syncError ?? "No settlement poller configured",
83
+ });
84
+ continue;
85
+ }
86
+ try {
87
+ const pollResult = await poller({
88
+ db,
89
+ invoice,
90
+ externalRef,
91
+ bindings: runtime.bindings ?? {},
92
+ });
93
+ const synced = await synchronizeExternalRef(db, invoice.id, externalRef, pollResult);
94
+ const paidAmountCents = normalizeMoney(pollResult.paidAmountCents);
95
+ const unpaidAmountCents = normalizeMoney(pollResult.unpaidAmountCents);
96
+ let newlyAppliedAmountCents = 0;
97
+ let createdPaymentId = null;
98
+ if (input.reconcilePayment && paidAmountCents !== null) {
99
+ const cappedPaidAmountCents = Math.min(invoice.totalCents, Math.max(0, paidAmountCents));
100
+ const outstandingAmountCents = Math.max(0, invoice.totalCents - invoice.paidCents);
101
+ newlyAppliedAmountCents = Math.min(outstandingAmountCents, Math.max(0, cappedPaidAmountCents - invoice.paidCents));
102
+ if (newlyAppliedAmountCents > 0) {
103
+ const payment = await financeService.createPayment(db, invoice.id, {
104
+ amountCents: newlyAppliedAmountCents,
105
+ currency: invoice.currency,
106
+ baseCurrency: invoice.baseCurrency ?? null,
107
+ baseAmountCents: invoice.baseCurrency && invoice.baseCurrency === invoice.currency
108
+ ? newlyAppliedAmountCents
109
+ : null,
110
+ fxRateSetId: invoice.fxRateSetId ?? null,
111
+ paymentMethod: input.paymentMethod,
112
+ paymentInstrumentId: null,
113
+ paymentAuthorizationId: null,
114
+ paymentCaptureId: null,
115
+ status: "completed",
116
+ referenceNumber: resolveReferenceNumber(input, pollResult, externalRef, invoice),
117
+ paymentDate: resolvePaymentDate(input, pollResult),
118
+ notes: input.notes ??
119
+ `Settlement reconciled from ${externalRef.provider} external reference`,
120
+ });
121
+ createdPaymentId = payment?.id ?? null;
122
+ invoice = (await financeService.getInvoiceById(db, invoice.id)) ?? invoice;
123
+ }
124
+ }
125
+ results.push({
126
+ provider: externalRef.provider,
127
+ externalRefId: synced.row?.id ?? externalRef.id,
128
+ externalId: synced.row?.externalId ?? externalRef.externalId ?? null,
129
+ externalNumber: synced.row?.externalNumber ?? externalRef.externalNumber ?? null,
130
+ externalUrl: synced.row?.externalUrl ?? externalRef.externalUrl ?? null,
131
+ status: synced.row?.status ?? externalRef.status ?? null,
132
+ paidAmountCents,
133
+ unpaidAmountCents,
134
+ syncedAt: toIsoString(synced.row?.syncedAt) ?? synced.syncedAt,
135
+ settledAt: toIsoString(pollResult.settledAt),
136
+ createdPaymentId,
137
+ newlyAppliedAmountCents,
138
+ syncError: synced.row?.syncError ?? pollResult.syncError ?? null,
139
+ });
140
+ }
141
+ catch (error) {
142
+ const message = error instanceof Error ? error.message : "Settlement polling failed";
143
+ const synced = await synchronizeExternalRef(db, invoice.id, externalRef, {
144
+ syncError: message,
145
+ });
146
+ results.push({
147
+ provider: externalRef.provider,
148
+ externalRefId: synced.row?.id ?? externalRef.id,
149
+ externalId: synced.row?.externalId ?? externalRef.externalId ?? null,
150
+ externalNumber: synced.row?.externalNumber ?? externalRef.externalNumber ?? null,
151
+ externalUrl: synced.row?.externalUrl ?? externalRef.externalUrl ?? null,
152
+ status: synced.row?.status ?? externalRef.status ?? null,
153
+ paidAmountCents: null,
154
+ unpaidAmountCents: null,
155
+ syncedAt: toIsoString(synced.row?.syncedAt) ?? synced.syncedAt,
156
+ settledAt: null,
157
+ createdPaymentId: null,
158
+ newlyAppliedAmountCents: 0,
159
+ syncError: synced.row?.syncError ?? message,
160
+ });
161
+ }
162
+ }
163
+ invoice = (await financeService.getInvoiceById(db, invoice.id)) ?? invoice;
164
+ return {
165
+ invoiceId: invoice.id,
166
+ invoiceStatus: invoice.status,
167
+ paidCents: invoice.paidCents,
168
+ balanceDueCents: invoice.balanceDueCents,
169
+ results,
170
+ };
171
+ },
172
+ };