@fabriktor/fx 0.0.128 → 0.0.129
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/dist/src/fx.js +3 -0
- package/dist/src/payments/payments.d.ts +82 -8
- package/dist/src/payments/payments.js +328 -15
- package/package.json +3 -3
package/dist/src/fx.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { type CreditLedgerEntry, type Payment, type PmtAccount, type PmtBankAccount, type PmtCard, type PmtCustomer, type PmtPublicSession, type PmtRefund } from "@fabriktor/schema";
|
|
2
|
-
import { type PaymentsOperator } from "@fabriktor/client";
|
|
3
|
-
import { type OwnerSearchOptions, type SearchPage } from "../core/search.js";
|
|
1
|
+
import { PaymentEntityTypeBill, PaymentEntityTypePayroll, type Bill, type Payroll, PSPPaymentStatusPending, PSPPaymentStatusPartial, PSPPaymentStatusFull, type CreditLedgerEntry, type Payment, type PmtAccount, type PmtBankAccount, type PmtCard, type PmtCustomer, type PmtPublicSession, type PmtRefund } from "@fabriktor/schema";
|
|
2
|
+
import { type PaymentsOperator, type BillableOperator, type PayrollsOperator } from "@fabriktor/client";
|
|
3
|
+
import { type SearchOptions, type OwnerSearchOptions, type SearchPage } from "../core/search.js";
|
|
4
|
+
import { EventBus } from "../core/events.js";
|
|
5
|
+
import type { MultiCreateAttempt, MultiCreateCheckpoint } from "../core/multi-create.js";
|
|
4
6
|
type PurchaseCreditsPayload = Parameters<PaymentsOperator["purchaseCredits"]>[0];
|
|
5
7
|
type OpenPublicSessionPayload = Parameters<PaymentsOperator["openPmtPublicSession"]>[0];
|
|
6
8
|
type PaymentFromPublicSessionPayload = Parameters<PaymentsOperator["paymentFromPublicSession"]>[0];
|
|
@@ -21,16 +23,73 @@ type PmtPublicSessionInsertOptions = Parameters<PaymentsOperator["insertOnePmtPu
|
|
|
21
23
|
type PmtPublicSessionReplaceOptions = Parameters<PaymentsOperator["replaceOnePmtPublicSession"]>[0];
|
|
22
24
|
type CreditLedgerEntryInsertOptions = Parameters<PaymentsOperator["insertOneCreditLedgerEntry"]>[0];
|
|
23
25
|
type CreditLedgerEntryReplaceOptions = Parameters<PaymentsOperator["replaceOneCreditLedgerEntry"]>[0];
|
|
24
|
-
export type
|
|
26
|
+
export type EntityPaymentScope = {
|
|
25
27
|
owner_id: string;
|
|
26
|
-
entity_id: string;
|
|
27
|
-
type: Payment["type"];
|
|
28
28
|
recipient_id?: string;
|
|
29
|
+
} | {
|
|
30
|
+
owner_id?: string;
|
|
31
|
+
recipient_id: string;
|
|
32
|
+
};
|
|
33
|
+
export type EntityPaymentStatus = typeof PSPPaymentStatusPending | typeof PSPPaymentStatusPartial | typeof PSPPaymentStatusFull;
|
|
34
|
+
export type EntityPaymentHistoryOptions = EntityPaymentScope & {
|
|
35
|
+
entity_id: string | readonly string[];
|
|
36
|
+
type: Payment["type"];
|
|
37
|
+
psp_payment_status?: EntityPaymentStatus | readonly EntityPaymentStatus[];
|
|
29
38
|
signal?: AbortSignal;
|
|
30
39
|
};
|
|
31
|
-
export type
|
|
40
|
+
export type PaymentEntity = {
|
|
41
|
+
id: string;
|
|
42
|
+
currency: string;
|
|
43
|
+
original_amount: number;
|
|
44
|
+
};
|
|
45
|
+
export type ResolveEntityPaymentsOptions = EntityPaymentScope & Pick<EntityPaymentHistoryOptions, "type" | "psp_payment_status" | "signal"> & {
|
|
46
|
+
entities: readonly PaymentEntity[];
|
|
47
|
+
};
|
|
48
|
+
export type EntityPayments = {
|
|
49
|
+
entity: PaymentEntity;
|
|
50
|
+
payments: Payment[];
|
|
51
|
+
matching_payments: Payment[];
|
|
52
|
+
paid_amount: number;
|
|
53
|
+
remaining_amount: number;
|
|
54
|
+
status: EntityPaymentStatus;
|
|
55
|
+
has_pending: boolean;
|
|
56
|
+
};
|
|
57
|
+
export declare const PaymentsEvents: {
|
|
58
|
+
Changed: import("../index.js").EventKey<Payment>;
|
|
59
|
+
};
|
|
60
|
+
export type PayableEntityType = typeof PaymentEntityTypeBill | typeof PaymentEntityTypePayroll;
|
|
61
|
+
export type PayableEntity = PaymentEntity & {
|
|
62
|
+
type: PayableEntityType;
|
|
63
|
+
recipient_id: string;
|
|
64
|
+
record: Bill | Payroll;
|
|
65
|
+
};
|
|
66
|
+
export type PayableEntityBalance = EntityPayments & {
|
|
67
|
+
entity: PayableEntity;
|
|
68
|
+
};
|
|
69
|
+
export type SearchPayableEntitiesOptions = SearchOptions & {
|
|
70
|
+
owner_id: string;
|
|
71
|
+
type: PayableEntityType;
|
|
72
|
+
};
|
|
73
|
+
export type ResolvePayableEntityOptions = {
|
|
74
|
+
owner_id: string;
|
|
75
|
+
entity_id: string;
|
|
76
|
+
type: PayableEntityType;
|
|
77
|
+
signal?: AbortSignal;
|
|
78
|
+
};
|
|
79
|
+
export type CreateEntityPaymentOptions = {
|
|
80
|
+
owner_id: string;
|
|
81
|
+
entity_id: string;
|
|
82
|
+
type: PayableEntityType;
|
|
83
|
+
remaining_amount: number;
|
|
84
|
+
payer_payment_method_psp_id: string;
|
|
85
|
+
return_url: string;
|
|
86
|
+
};
|
|
87
|
+
export type CreatePaymentOptions = PaymentInsertOptions & MultiCreateCheckpoint;
|
|
32
88
|
export type ReplacePaymentOptions = PaymentReplaceOptions;
|
|
33
|
-
export type SearchPaymentsOptions = OwnerSearchOptions
|
|
89
|
+
export type SearchPaymentsOptions = OwnerSearchOptions & {
|
|
90
|
+
type?: Payment["type"] | Payment["type"][];
|
|
91
|
+
kind?: Payment["kind"];
|
|
92
|
+
};
|
|
34
93
|
export type SearchPaymentsPage = SearchPage<Payment>;
|
|
35
94
|
export type CreatePmtAccountOptions = PmtAccountInsertOptions;
|
|
36
95
|
export type ContinuePmtAccountOnboardingOptions = PmtAccountOnboardingOptions;
|
|
@@ -110,9 +169,16 @@ export type OpenPublicPaymentSessionResult = {
|
|
|
110
169
|
};
|
|
111
170
|
export type PaymentsFXOptions = {
|
|
112
171
|
payments: PaymentsOperator;
|
|
172
|
+
billable?: Pick<BillableOperator, "searchManyBills" | "findOneBill">;
|
|
173
|
+
payrolls?: Pick<PayrollsOperator, "searchManyPayrolls" | "findOnePayroll">;
|
|
174
|
+
events?: EventBus;
|
|
113
175
|
};
|
|
114
176
|
export interface PaymentsFXOperator {
|
|
177
|
+
resolvePayableEntity(opts: ResolvePayableEntityOptions): Promise<PayableEntityBalance>;
|
|
178
|
+
searchPayableEntities(opts: SearchPayableEntitiesOptions): Promise<SearchPage<PayableEntityBalance>>;
|
|
179
|
+
createEntityPayment(opts: CreateEntityPaymentOptions, attempt: MultiCreateAttempt): Promise<Payment>;
|
|
115
180
|
entityPaymentHistory(opts: EntityPaymentHistoryOptions): Promise<Payment[]>;
|
|
181
|
+
resolveEntityPayments(opts: ResolveEntityPaymentsOptions): Promise<EntityPayments[]>;
|
|
116
182
|
createPayment(opts: CreatePaymentOptions): Promise<Payment>;
|
|
117
183
|
replacePayment(opts: ReplacePaymentOptions): Promise<Payment>;
|
|
118
184
|
searchPayments(opts: SearchPaymentsOptions): Promise<SearchPaymentsPage>;
|
|
@@ -149,9 +215,16 @@ export interface PaymentsFXOperator {
|
|
|
149
215
|
}
|
|
150
216
|
export declare class PaymentsFX implements PaymentsFXOperator {
|
|
151
217
|
private payments;
|
|
218
|
+
private billable;
|
|
219
|
+
private payrolls;
|
|
220
|
+
private readonly events;
|
|
152
221
|
private submittedPublicSessionIDs;
|
|
153
222
|
constructor(opts: PaymentsFXOptions);
|
|
154
223
|
entityPaymentHistory(opts: EntityPaymentHistoryOptions): Promise<Payment[]>;
|
|
224
|
+
resolveEntityPayments(opts: ResolveEntityPaymentsOptions): Promise<EntityPayments[]>;
|
|
225
|
+
searchPayableEntities(opts: SearchPayableEntitiesOptions): Promise<SearchPage<PayableEntityBalance>>;
|
|
226
|
+
resolvePayableEntity(opts: ResolvePayableEntityOptions): Promise<PayableEntityBalance>;
|
|
227
|
+
createEntityPayment(opts: CreateEntityPaymentOptions, attempt: MultiCreateAttempt): Promise<Payment>;
|
|
155
228
|
createPayment(opts: CreatePaymentOptions): Promise<Payment>;
|
|
156
229
|
replacePayment(opts: ReplacePaymentOptions): Promise<Payment>;
|
|
157
230
|
searchPayments(opts: SearchPaymentsOptions): Promise<SearchPaymentsPage>;
|
|
@@ -196,4 +269,5 @@ export declare class PaymentsFX implements PaymentsFXOperator {
|
|
|
196
269
|
private openPublicPaymentSession;
|
|
197
270
|
}
|
|
198
271
|
export declare function newPaymentsFX(opts: PaymentsFXOptions): PaymentsFX;
|
|
272
|
+
export declare function isPaymentPending(payment: Payment): boolean;
|
|
199
273
|
export {};
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
4
4
|
// not use this file except in compliance with the License. You may obtain
|
|
5
5
|
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
-
import { CreditLedgerSourceAndroidIAP, CreditLedgerSourceAppleIAP, CreditLedgerSourceStripe, ZeroID, PaymentKindMain, } from "@fabriktor/schema";
|
|
7
|
-
import { queryParams } from "@fabriktor/client";
|
|
6
|
+
import { CreditLedgerSourceAndroidIAP, CreditLedgerSourceAppleIAP, CreditLedgerSourceStripe, ZeroID, PaymentKindMain, PaymentEntityTypeBill, PaymentEntityTypePayroll, PSPActionableStatusSucceeded, PSPActionableStatusFailed, PSPActionableStatusCanceled, PSPPaymentStatusPending, PSPPaymentStatusPartial, PSPPaymentStatusFull, } from "@fabriktor/schema";
|
|
7
|
+
import { queryParams, } from "@fabriktor/client";
|
|
8
8
|
import { errResolveFailed, errValidation } from "../core/err.js";
|
|
9
|
-
import { requiredOperationID, resolvedID } from "../core/id.js";
|
|
10
|
-
import { runOwnerSearch, } from "../core/search.js";
|
|
9
|
+
import { normalizeIDs, requiredOperationID, resolvedID } from "../core/id.js";
|
|
10
|
+
import { runOwnerSearch, runSearch, } from "../core/search.js";
|
|
11
|
+
import { createEvent, EventBus } from "../core/events.js";
|
|
12
|
+
const entityPaymentQuerySize = 30;
|
|
11
13
|
const publicAmountPattern = /^\d+$/;
|
|
12
14
|
const resourcePayment = "payment";
|
|
13
15
|
const resourcePmtAccount = "pmt_account";
|
|
@@ -20,6 +22,17 @@ const resourcePmtPublicSession = "pmt_public_session";
|
|
|
20
22
|
const resourceCreditLedgerEntry = "credit_ledger_entry";
|
|
21
23
|
const resourceStripeCreditPayment = "stripe credit payment";
|
|
22
24
|
const resourcePublicPaymentSession = "public payment session";
|
|
25
|
+
const msgPayableSourceMissing = "Payable entity source is unavailable.";
|
|
26
|
+
const msgPayableBalanceChanged = "Payment balance changed or a payment is still pending. Refresh before paying.";
|
|
27
|
+
const msgPaymentMethodRequired = "A payment method is required.";
|
|
28
|
+
const msgPayableUnavailable = "This entity is not available for payment.";
|
|
29
|
+
const msgPaymentScopeMissing = "Payment history requires a payer or recipient scope and an entity type.";
|
|
30
|
+
const msgPaymentHistoryIncomplete = "Payment history response is incomplete.";
|
|
31
|
+
const msgPaymentEntityInvalid = "Payment resolution requires a valid entity, currency, and amount.";
|
|
32
|
+
const msgPaymentEntityConflict = "Payment resolution received conflicting entity amounts.";
|
|
33
|
+
const msgPaymentCurrencyMismatch = "Payment currency differs from the entity currency.";
|
|
34
|
+
const msgPaymentAmountInvalid = "Payment amount is invalid.";
|
|
35
|
+
const msgPaymentStatusesInvalid = "Select at least one valid payment status, or omit the status filter.";
|
|
23
36
|
const msgCreatedPaymentIDMissing = "Created payment response did not contain an ID.";
|
|
24
37
|
const msgPaymentMissing = "Payment could not be resolved after the mutation.";
|
|
25
38
|
const msgCreatedPmtAccountIDMissing = "Created payment account response did not contain an ID.";
|
|
@@ -47,6 +60,9 @@ const msgPublicSessionConsumed = "Public payment session has already been consum
|
|
|
47
60
|
const msgPublicAmountInvalid = "Public payment amount must be a positive integer.";
|
|
48
61
|
const msgPublicBalanceInvalid = "Public payment session balance is invalid.";
|
|
49
62
|
const msgPublicAmountExceedsBalance = "Public payment amount cannot exceed the session balance.";
|
|
63
|
+
export const PaymentsEvents = {
|
|
64
|
+
Changed: createEvent(),
|
|
65
|
+
};
|
|
50
66
|
export const PmtAccountAccessStatus = {
|
|
51
67
|
Missing: "missing",
|
|
52
68
|
OnboardingRequired: "onboarding_required",
|
|
@@ -71,8 +87,7 @@ export function pmtAccountAccess(account) {
|
|
|
71
87
|
if (account === null) {
|
|
72
88
|
return { account, status: PmtAccountAccessStatus.Missing, ready: false };
|
|
73
89
|
}
|
|
74
|
-
if (account.is_currently_due ||
|
|
75
|
-
account.is_external_account_due) {
|
|
90
|
+
if (account.is_currently_due || account.is_external_account_due) {
|
|
76
91
|
return {
|
|
77
92
|
account,
|
|
78
93
|
status: PmtAccountAccessStatus.RequirementsDue,
|
|
@@ -96,34 +111,289 @@ export const PublicPaymentSessionStatus = {
|
|
|
96
111
|
};
|
|
97
112
|
export class PaymentsFX {
|
|
98
113
|
payments;
|
|
114
|
+
billable;
|
|
115
|
+
payrolls;
|
|
116
|
+
events;
|
|
99
117
|
submittedPublicSessionIDs;
|
|
100
118
|
constructor(opts) {
|
|
101
119
|
this.payments = opts.payments;
|
|
120
|
+
this.billable = opts.billable;
|
|
121
|
+
this.payrolls = opts.payrolls;
|
|
122
|
+
this.events = opts.events ?? new EventBus();
|
|
102
123
|
this.submittedPublicSessionIDs = new Set();
|
|
103
124
|
}
|
|
104
125
|
async entityPaymentHistory(opts) {
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
const owner_id = resolvedID(opts.owner_id);
|
|
127
|
+
const recipient_id = resolvedID(opts.recipient_id);
|
|
128
|
+
if ((!owner_id && !recipient_id) || !opts.type.trim()) {
|
|
129
|
+
throw errValidation({ msg: msgPaymentScopeMissing });
|
|
130
|
+
}
|
|
131
|
+
const ids = normalizeIDs(typeof opts.entity_id === "string" ? [opts.entity_id] : opts.entity_id);
|
|
132
|
+
const statuses = entityPaymentStatuses(opts.psp_payment_status);
|
|
133
|
+
const records = new Map();
|
|
134
|
+
for (let offset = 0; offset < ids.length; offset += entityPaymentQuerySize) {
|
|
135
|
+
opts.signal?.throwIfAborted();
|
|
136
|
+
const response = await this.payments.queryPayments({
|
|
137
|
+
signal: opts.signal,
|
|
138
|
+
query: queryParams({
|
|
139
|
+
owner_id,
|
|
140
|
+
recipient_id,
|
|
141
|
+
entity_id: ids.length === 1
|
|
142
|
+
? ids[0]
|
|
143
|
+
: ids.slice(offset, offset + entityPaymentQuerySize),
|
|
144
|
+
type: opts.type,
|
|
145
|
+
kind: PaymentKindMain,
|
|
146
|
+
...(statuses === undefined ? {} : { psp_payment_status: statuses }),
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
opts.signal?.throwIfAborted();
|
|
150
|
+
if (!Array.isArray(response.value)) {
|
|
151
|
+
throw errResolveFailed({
|
|
152
|
+
resource: resourcePayment,
|
|
153
|
+
msg: msgPaymentHistoryIncomplete,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// Retain each entity's backend order and ignore duplicate documents.
|
|
157
|
+
for (const payment of response.value)
|
|
158
|
+
records.set(payment.id, payment);
|
|
159
|
+
}
|
|
160
|
+
return [...records.values()];
|
|
161
|
+
}
|
|
162
|
+
async resolveEntityPayments(opts) {
|
|
163
|
+
const { entities, psp_payment_status, ...scope } = opts;
|
|
164
|
+
const statuses = entityPaymentStatuses(psp_payment_status);
|
|
165
|
+
const unique = new Map();
|
|
166
|
+
for (const entity of entities) {
|
|
167
|
+
if (!resolvedID(entity.id) ||
|
|
168
|
+
!entity.currency.trim() ||
|
|
169
|
+
!Number.isSafeInteger(entity.original_amount) ||
|
|
170
|
+
entity.original_amount < 0) {
|
|
171
|
+
throw errValidation({ msg: msgPaymentEntityInvalid });
|
|
172
|
+
}
|
|
173
|
+
const previous = unique.get(entity.id);
|
|
174
|
+
if (previous &&
|
|
175
|
+
(previous.currency !== entity.currency ||
|
|
176
|
+
previous.original_amount !== entity.original_amount)) {
|
|
177
|
+
throw errValidation({ msg: msgPaymentEntityConflict });
|
|
178
|
+
}
|
|
179
|
+
unique.set(entity.id, entity);
|
|
180
|
+
}
|
|
181
|
+
// Never infer unpaid from a filtered history: resolve coverage from all statuses.
|
|
182
|
+
const payments = await this.entityPaymentHistory({
|
|
183
|
+
...scope,
|
|
184
|
+
entity_id: [...unique.keys()],
|
|
185
|
+
});
|
|
186
|
+
const grouped = new Map();
|
|
187
|
+
for (const payment of payments) {
|
|
188
|
+
if (payment.kind !== PaymentKindMain ||
|
|
189
|
+
payment.type !== opts.type ||
|
|
190
|
+
!unique.has(payment.entity_id))
|
|
191
|
+
continue;
|
|
192
|
+
const group = grouped.get(payment.entity_id) ?? [];
|
|
193
|
+
group.push(payment);
|
|
194
|
+
grouped.set(payment.entity_id, group);
|
|
195
|
+
}
|
|
196
|
+
return [...unique.values()].map((entity) => {
|
|
197
|
+
const records = grouped.get(entity.id) ?? [];
|
|
198
|
+
let paid_amount = 0;
|
|
199
|
+
for (const payment of records) {
|
|
200
|
+
if (payment.currency.toUpperCase() !== entity.currency.toUpperCase()) {
|
|
201
|
+
throw errResolveFailed({
|
|
202
|
+
resource: resourcePayment,
|
|
203
|
+
msg: msgPaymentCurrencyMismatch,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (payment.psp_actionable_status !== PSPActionableStatusSucceeded)
|
|
207
|
+
continue;
|
|
208
|
+
const amount = payment.gross_amount.raw;
|
|
209
|
+
if (!Number.isSafeInteger(amount) ||
|
|
210
|
+
amount < 0 ||
|
|
211
|
+
!Number.isSafeInteger(paid_amount + amount)) {
|
|
212
|
+
throw errResolveFailed({
|
|
213
|
+
resource: resourcePayment,
|
|
214
|
+
msg: msgPaymentAmountInvalid,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
paid_amount += amount;
|
|
218
|
+
}
|
|
219
|
+
const remaining_amount = Math.max(0, entity.original_amount - paid_amount);
|
|
220
|
+
return {
|
|
221
|
+
entity,
|
|
222
|
+
payments: records,
|
|
223
|
+
matching_payments: statuses === undefined
|
|
224
|
+
? records
|
|
225
|
+
: records.filter((payment) => statuses.includes(payment.psp_payment_status)),
|
|
226
|
+
paid_amount,
|
|
227
|
+
remaining_amount,
|
|
228
|
+
status: remaining_amount === 0
|
|
229
|
+
? PSPPaymentStatusFull
|
|
230
|
+
: paid_amount > 0
|
|
231
|
+
? PSPPaymentStatusPartial
|
|
232
|
+
: PSPPaymentStatusPending,
|
|
233
|
+
has_pending: records.some(isPaymentPending),
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Scan real source pages until there are payable results or the source is exhausted.
|
|
238
|
+
// Settlement is a cross-service resolution, not a fabricated backend unpaid filter.
|
|
239
|
+
async searchPayableEntities(opts) {
|
|
240
|
+
if (!resolvedID(opts.owner_id))
|
|
241
|
+
throw errValidation({ msg: msgPaymentScopeMissing });
|
|
242
|
+
let page = opts.page;
|
|
243
|
+
while (true) {
|
|
244
|
+
opts.signal?.throwIfAborted();
|
|
245
|
+
let source;
|
|
246
|
+
if (opts.type === PaymentEntityTypeBill && this.billable) {
|
|
247
|
+
source = await runSearch({
|
|
248
|
+
...opts,
|
|
249
|
+
page,
|
|
250
|
+
query: queryParams({
|
|
251
|
+
client_id: opts.owner_id,
|
|
252
|
+
is_internal_client: true,
|
|
253
|
+
is_estimate: false,
|
|
254
|
+
is_canceled: false,
|
|
255
|
+
}),
|
|
256
|
+
search: (request) => this.billable.searchManyBills(request),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else if (opts.type === PaymentEntityTypePayroll && this.payrolls) {
|
|
260
|
+
source = await runOwnerSearch({
|
|
261
|
+
...opts,
|
|
262
|
+
page,
|
|
263
|
+
query: queryParams({
|
|
264
|
+
is_banked: false,
|
|
265
|
+
is_problematic: false,
|
|
266
|
+
}),
|
|
267
|
+
search: (request) => this.payrolls.searchManyPayrolls(request),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
throw errValidation({ msg: msgPayableSourceMissing });
|
|
272
|
+
const entities = source.results.map((record) => payableEntity(record, opts.type, opts.owner_id));
|
|
273
|
+
const balances = await this.resolveEntityPayments({
|
|
274
|
+
owner_id: opts.owner_id,
|
|
275
|
+
type: opts.type,
|
|
276
|
+
entities,
|
|
277
|
+
signal: opts.signal,
|
|
278
|
+
});
|
|
279
|
+
const results = balances.flatMap((balance, index) => balance.remaining_amount > 0 && !balance.has_pending
|
|
280
|
+
? [{ ...balance, entity: entities[index] }]
|
|
281
|
+
: []);
|
|
282
|
+
if (results.length || source.done) {
|
|
283
|
+
// Source counts include paid records and cannot describe the filtered result.
|
|
284
|
+
return {
|
|
285
|
+
...source,
|
|
286
|
+
results,
|
|
287
|
+
total_count: undefined,
|
|
288
|
+
last_page: undefined,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (source.next_page <= source.page)
|
|
292
|
+
throw errResolveFailed({
|
|
293
|
+
resource: resourcePayment,
|
|
294
|
+
msg: msgPaymentHistoryIncomplete,
|
|
295
|
+
});
|
|
296
|
+
page = source.next_page;
|
|
107
297
|
}
|
|
108
|
-
|
|
298
|
+
}
|
|
299
|
+
async resolvePayableEntity(opts) {
|
|
300
|
+
opts.signal?.throwIfAborted();
|
|
301
|
+
const record = opts.type === PaymentEntityTypeBill
|
|
302
|
+
? (await this.billable?.findOneBill({
|
|
303
|
+
id: opts.entity_id,
|
|
304
|
+
signal: opts.signal,
|
|
305
|
+
}))?.value
|
|
306
|
+
: opts.type === PaymentEntityTypePayroll
|
|
307
|
+
? (await this.payrolls?.findOnePayroll({
|
|
308
|
+
id: opts.entity_id,
|
|
309
|
+
signal: opts.signal,
|
|
310
|
+
}))?.value
|
|
311
|
+
: undefined;
|
|
312
|
+
if (!record)
|
|
313
|
+
throw errResolveFailed({
|
|
314
|
+
resource: resourcePayment,
|
|
315
|
+
msg: msgPaymentMissing,
|
|
316
|
+
});
|
|
317
|
+
const entity = payableEntity(record, opts.type, opts.owner_id);
|
|
318
|
+
const [balance] = await this.resolveEntityPayments({
|
|
319
|
+
owner_id: opts.owner_id,
|
|
320
|
+
type: opts.type,
|
|
321
|
+
entities: [entity],
|
|
109
322
|
signal: opts.signal,
|
|
110
|
-
query: queryParams({ owner_id: opts.owner_id, entity_id: opts.entity_id, type: opts.type, kind: PaymentKindMain, recipient_id: opts.recipient_id }),
|
|
111
323
|
});
|
|
112
|
-
|
|
113
|
-
|
|
324
|
+
if (!balance)
|
|
325
|
+
throw errResolveFailed({
|
|
326
|
+
resource: resourcePayment,
|
|
327
|
+
msg: msgPaymentHistoryIncomplete,
|
|
328
|
+
});
|
|
329
|
+
return { ...balance, entity };
|
|
330
|
+
}
|
|
331
|
+
async createEntityPayment(opts, attempt) {
|
|
332
|
+
if (attempt.created_id)
|
|
333
|
+
return this.findPayment(attempt.created_id, attempt.signal);
|
|
334
|
+
const balance = await this.resolvePayableEntity({
|
|
335
|
+
...opts,
|
|
336
|
+
signal: attempt.signal,
|
|
337
|
+
});
|
|
338
|
+
const { entity } = balance;
|
|
339
|
+
if (!balance ||
|
|
340
|
+
balance.has_pending ||
|
|
341
|
+
balance.remaining_amount <= 0 ||
|
|
342
|
+
balance.remaining_amount !== opts.remaining_amount) {
|
|
343
|
+
throw errValidation({
|
|
344
|
+
msg: msgPayableBalanceChanged,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (!resolvedID(opts.payer_payment_method_psp_id))
|
|
348
|
+
throw errValidation({ msg: msgPaymentMethodRequired });
|
|
349
|
+
const amount = {
|
|
350
|
+
raw: balance.remaining_amount,
|
|
351
|
+
text: String(balance.remaining_amount),
|
|
352
|
+
};
|
|
353
|
+
return this.createPayment({
|
|
354
|
+
...attempt,
|
|
355
|
+
in: {
|
|
356
|
+
psp_confirm: true,
|
|
357
|
+
is_manual: false,
|
|
358
|
+
currency: entity.currency,
|
|
359
|
+
entity_id: entity.id,
|
|
360
|
+
type: entity.type,
|
|
361
|
+
recipient_id: entity.recipient_id,
|
|
362
|
+
payer_payment_method_psp_id: opts.payer_payment_method_psp_id,
|
|
363
|
+
return_url: opts.return_url,
|
|
364
|
+
use_psp_sdk: true,
|
|
365
|
+
original_amount: {
|
|
366
|
+
raw: entity.original_amount,
|
|
367
|
+
text: String(entity.original_amount),
|
|
368
|
+
},
|
|
369
|
+
original_balance_amount: amount,
|
|
370
|
+
gross_amount: amount,
|
|
371
|
+
is_off_session: false,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
114
374
|
}
|
|
115
375
|
async createPayment(opts) {
|
|
376
|
+
if (opts.created_id)
|
|
377
|
+
return this.findPayment(opts.created_id, opts.signal);
|
|
378
|
+
opts.signal?.throwIfAborted();
|
|
379
|
+
opts.before_request?.();
|
|
116
380
|
const out = await this.payments.insertOnePayment(opts);
|
|
117
381
|
const id = requiredOperationID(out, msgCreatedPaymentIDMissing);
|
|
118
|
-
|
|
382
|
+
opts.on_created?.(id);
|
|
383
|
+
const payment = await this.findPayment(id, opts.signal);
|
|
384
|
+
this.events.publish(PaymentsEvents.Changed, payment);
|
|
385
|
+
return payment;
|
|
119
386
|
}
|
|
120
387
|
async replacePayment(opts) {
|
|
121
388
|
await this.payments.replaceOnePayment(opts);
|
|
122
|
-
|
|
389
|
+
const payment = await this.findPayment(opts.id);
|
|
390
|
+
this.events.publish(PaymentsEvents.Changed, payment);
|
|
391
|
+
return payment;
|
|
123
392
|
}
|
|
124
393
|
async searchPayments(opts) {
|
|
125
394
|
return await runOwnerSearch({
|
|
126
395
|
...opts,
|
|
396
|
+
query: queryParams({ type: opts.type, kind: opts.kind }),
|
|
127
397
|
search: (searchOpts) => this.payments.searchManyPayments(searchOpts),
|
|
128
398
|
});
|
|
129
399
|
}
|
|
@@ -388,9 +658,10 @@ export class PaymentsFX {
|
|
|
388
658
|
throw err;
|
|
389
659
|
}
|
|
390
660
|
}
|
|
391
|
-
async findPayment(id) {
|
|
661
|
+
async findPayment(id, signal) {
|
|
392
662
|
const out = await this.payments.findOnePayment({
|
|
393
663
|
id,
|
|
664
|
+
signal,
|
|
394
665
|
});
|
|
395
666
|
const payment = out.value;
|
|
396
667
|
if (payment === undefined) {
|
|
@@ -597,3 +868,45 @@ function zeroGoogleIAP() {
|
|
|
597
868
|
kind: "",
|
|
598
869
|
};
|
|
599
870
|
}
|
|
871
|
+
// A coverage flag alone does not establish the provider outcome.
|
|
872
|
+
export function isPaymentPending(payment) {
|
|
873
|
+
return (payment.psp_actionable_status !== PSPActionableStatusSucceeded &&
|
|
874
|
+
payment.psp_actionable_status !== PSPActionableStatusFailed &&
|
|
875
|
+
payment.psp_actionable_status !== PSPActionableStatusCanceled);
|
|
876
|
+
}
|
|
877
|
+
function entityPaymentStatuses(value) {
|
|
878
|
+
if (value === undefined)
|
|
879
|
+
return undefined;
|
|
880
|
+
const values = [...new Set(typeof value === "string" ? [value] : value)];
|
|
881
|
+
if (!values.length ||
|
|
882
|
+
values.some((status) => status !== PSPPaymentStatusPending &&
|
|
883
|
+
status !== PSPPaymentStatusPartial &&
|
|
884
|
+
status !== PSPPaymentStatusFull)) {
|
|
885
|
+
throw errValidation({ msg: msgPaymentStatusesInvalid });
|
|
886
|
+
}
|
|
887
|
+
return values;
|
|
888
|
+
}
|
|
889
|
+
function payableEntity(record, type, ownerID) {
|
|
890
|
+
const bill = "is_estimate" in record;
|
|
891
|
+
if (!resolvedID(ownerID) ||
|
|
892
|
+
(type === PaymentEntityTypeBill
|
|
893
|
+
? !bill ||
|
|
894
|
+
!record.is_internal_client ||
|
|
895
|
+
record.client_id !== ownerID ||
|
|
896
|
+
record.is_estimate ||
|
|
897
|
+
record.is_canceled
|
|
898
|
+
: bill ||
|
|
899
|
+
record.owner_id !== ownerID ||
|
|
900
|
+
record.is_banked ||
|
|
901
|
+
record.is_problematic)) {
|
|
902
|
+
throw errValidation({ msg: msgPayableUnavailable });
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
id: record.id,
|
|
906
|
+
type,
|
|
907
|
+
recipient_id: bill ? record.owner_id : record.employee_id,
|
|
908
|
+
currency: record.currency,
|
|
909
|
+
original_amount: bill ? record.total.total.raw : record.net_amount.raw,
|
|
910
|
+
record,
|
|
911
|
+
};
|
|
912
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabriktor/fx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.129",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"typescript": "^6.0.3"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@fabriktor/client": "0.0.
|
|
20
|
+
"@fabriktor/client": "0.0.92",
|
|
21
21
|
"@fabriktor/fx": "0.0.94",
|
|
22
|
-
"@fabriktor/schema": "0.0.
|
|
22
|
+
"@fabriktor/schema": "0.0.66"
|
|
23
23
|
}
|
|
24
24
|
}
|