@fabriktor/fx 0.0.128 → 0.0.130

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 CHANGED
@@ -79,6 +79,9 @@ export class FX {
79
79
  });
80
80
  this.payments = newPaymentsFX({
81
81
  payments: opts.payments,
82
+ billable: opts.billable,
83
+ payrolls: opts.payrolls,
84
+ events: this.events,
82
85
  });
83
86
  this.payrolls = newPayrollsFX({
84
87
  payrolls: opts.payrolls,
@@ -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 EntityPaymentHistoryOptions = {
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 CreatePaymentOptions = PaymentInsertOptions;
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,288 @@ 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
- if (!resolvedID(opts.owner_id) || !resolvedID(opts.entity_id) || !opts.type.trim()) {
106
- throw errValidation({ field: "entity_id", msg: "Payment history requires an owner, entity, and type." });
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_problematic: false,
265
+ }),
266
+ search: (request) => this.payrolls.searchManyPayrolls(request),
267
+ });
268
+ }
269
+ else
270
+ throw errValidation({ msg: msgPayableSourceMissing });
271
+ const entities = source.results.map((record) => payableEntity(record, opts.type, opts.owner_id));
272
+ const balances = await this.resolveEntityPayments({
273
+ owner_id: opts.owner_id,
274
+ type: opts.type,
275
+ entities,
276
+ signal: opts.signal,
277
+ });
278
+ const results = balances.flatMap((balance, index) => balance.remaining_amount > 0 && !balance.has_pending
279
+ ? [{ ...balance, entity: entities[index] }]
280
+ : []);
281
+ if (results.length || source.done) {
282
+ // Source counts include paid records and cannot describe the filtered result.
283
+ return {
284
+ ...source,
285
+ results,
286
+ total_count: undefined,
287
+ last_page: undefined,
288
+ };
289
+ }
290
+ if (source.next_page <= source.page)
291
+ throw errResolveFailed({
292
+ resource: resourcePayment,
293
+ msg: msgPaymentHistoryIncomplete,
294
+ });
295
+ page = source.next_page;
107
296
  }
108
- const response = await this.payments.queryPayments({
297
+ }
298
+ async resolvePayableEntity(opts) {
299
+ opts.signal?.throwIfAborted();
300
+ const record = opts.type === PaymentEntityTypeBill
301
+ ? (await this.billable?.findOneBill({
302
+ id: opts.entity_id,
303
+ signal: opts.signal,
304
+ }))?.value
305
+ : opts.type === PaymentEntityTypePayroll
306
+ ? (await this.payrolls?.findOnePayroll({
307
+ id: opts.entity_id,
308
+ signal: opts.signal,
309
+ }))?.value
310
+ : undefined;
311
+ if (!record)
312
+ throw errResolveFailed({
313
+ resource: resourcePayment,
314
+ msg: msgPaymentMissing,
315
+ });
316
+ const entity = payableEntity(record, opts.type, opts.owner_id);
317
+ const [balance] = await this.resolveEntityPayments({
318
+ owner_id: opts.owner_id,
319
+ type: opts.type,
320
+ entities: [entity],
109
321
  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
322
  });
112
- // Preserve the backend's chronological order; do not infer settlement from PSP payment balance flags.
113
- return response.value ?? [];
323
+ if (!balance)
324
+ throw errResolveFailed({
325
+ resource: resourcePayment,
326
+ msg: msgPaymentHistoryIncomplete,
327
+ });
328
+ return { ...balance, entity };
329
+ }
330
+ async createEntityPayment(opts, attempt) {
331
+ if (attempt.created_id)
332
+ return this.findPayment(attempt.created_id, attempt.signal);
333
+ const balance = await this.resolvePayableEntity({
334
+ ...opts,
335
+ signal: attempt.signal,
336
+ });
337
+ const { entity } = balance;
338
+ if (!balance ||
339
+ balance.has_pending ||
340
+ balance.remaining_amount <= 0 ||
341
+ balance.remaining_amount !== opts.remaining_amount) {
342
+ throw errValidation({
343
+ msg: msgPayableBalanceChanged,
344
+ });
345
+ }
346
+ if (!resolvedID(opts.payer_payment_method_psp_id))
347
+ throw errValidation({ msg: msgPaymentMethodRequired });
348
+ const amount = {
349
+ raw: balance.remaining_amount,
350
+ text: String(balance.remaining_amount),
351
+ };
352
+ return this.createPayment({
353
+ ...attempt,
354
+ in: {
355
+ psp_confirm: true,
356
+ is_manual: false,
357
+ currency: entity.currency,
358
+ entity_id: entity.id,
359
+ type: entity.type,
360
+ recipient_id: entity.recipient_id,
361
+ payer_payment_method_psp_id: opts.payer_payment_method_psp_id,
362
+ return_url: opts.return_url,
363
+ use_psp_sdk: true,
364
+ original_amount: {
365
+ raw: entity.original_amount,
366
+ text: String(entity.original_amount),
367
+ },
368
+ original_balance_amount: amount,
369
+ gross_amount: amount,
370
+ is_off_session: false,
371
+ },
372
+ });
114
373
  }
115
374
  async createPayment(opts) {
375
+ if (opts.created_id)
376
+ return this.findPayment(opts.created_id, opts.signal);
377
+ opts.signal?.throwIfAborted();
378
+ opts.before_request?.();
116
379
  const out = await this.payments.insertOnePayment(opts);
117
380
  const id = requiredOperationID(out, msgCreatedPaymentIDMissing);
118
- return await this.findPayment(id);
381
+ opts.on_created?.(id);
382
+ const payment = await this.findPayment(id, opts.signal);
383
+ this.events.publish(PaymentsEvents.Changed, payment);
384
+ return payment;
119
385
  }
120
386
  async replacePayment(opts) {
121
387
  await this.payments.replaceOnePayment(opts);
122
- return await this.findPayment(opts.id);
388
+ const payment = await this.findPayment(opts.id);
389
+ this.events.publish(PaymentsEvents.Changed, payment);
390
+ return payment;
123
391
  }
124
392
  async searchPayments(opts) {
125
393
  return await runOwnerSearch({
126
394
  ...opts,
395
+ query: queryParams({ type: opts.type, kind: opts.kind }),
127
396
  search: (searchOpts) => this.payments.searchManyPayments(searchOpts),
128
397
  });
129
398
  }
@@ -388,9 +657,10 @@ export class PaymentsFX {
388
657
  throw err;
389
658
  }
390
659
  }
391
- async findPayment(id) {
660
+ async findPayment(id, signal) {
392
661
  const out = await this.payments.findOnePayment({
393
662
  id,
663
+ signal,
394
664
  });
395
665
  const payment = out.value;
396
666
  if (payment === undefined) {
@@ -597,3 +867,44 @@ function zeroGoogleIAP() {
597
867
  kind: "",
598
868
  };
599
869
  }
870
+ // A coverage flag alone does not establish the provider outcome.
871
+ export function isPaymentPending(payment) {
872
+ return (payment.psp_actionable_status !== PSPActionableStatusSucceeded &&
873
+ payment.psp_actionable_status !== PSPActionableStatusFailed &&
874
+ payment.psp_actionable_status !== PSPActionableStatusCanceled);
875
+ }
876
+ function entityPaymentStatuses(value) {
877
+ if (value === undefined)
878
+ return undefined;
879
+ const values = [...new Set(typeof value === "string" ? [value] : value)];
880
+ if (!values.length ||
881
+ values.some((status) => status !== PSPPaymentStatusPending &&
882
+ status !== PSPPaymentStatusPartial &&
883
+ status !== PSPPaymentStatusFull)) {
884
+ throw errValidation({ msg: msgPaymentStatusesInvalid });
885
+ }
886
+ return values;
887
+ }
888
+ function payableEntity(record, type, ownerID) {
889
+ const bill = "is_estimate" in record;
890
+ if (!resolvedID(ownerID) ||
891
+ (type === PaymentEntityTypeBill
892
+ ? !bill ||
893
+ !record.is_internal_client ||
894
+ record.client_id !== ownerID ||
895
+ record.is_estimate ||
896
+ record.is_canceled
897
+ : bill ||
898
+ record.owner_id !== ownerID ||
899
+ record.is_problematic)) {
900
+ throw errValidation({ msg: msgPayableUnavailable });
901
+ }
902
+ return {
903
+ id: record.id,
904
+ type,
905
+ recipient_id: bill ? record.owner_id : record.employee_id,
906
+ currency: record.currency,
907
+ original_amount: bill ? record.total.total.raw : record.net_amount.raw,
908
+ record,
909
+ };
910
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.128",
3
+ "version": "0.0.130",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
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.91",
20
+ "@fabriktor/client": "0.0.92",
21
21
  "@fabriktor/fx": "0.0.94",
22
- "@fabriktor/schema": "0.0.65"
22
+ "@fabriktor/schema": "0.0.66"
23
23
  }
24
24
  }