@fabriktor/fx 0.0.132 → 0.0.134

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.
@@ -48,6 +48,8 @@ export declare class MultiCreatePool {
48
48
  export type MultiCreateOptions<Input, Result> = {
49
49
  run: (input: Input, attempt: MultiCreateAttempt) => Promise<Result>;
50
50
  pool?: MultiCreatePool;
51
+ check?: (inputs: readonly Input[], signal: AbortSignal) => Promise<void>;
52
+ on_blocked?: (entry: MultiCreateEntry<Input, Result>) => void;
51
53
  retry_same_request?: boolean;
52
54
  before_request?: () => void;
53
55
  on_failed?: (entry: MultiCreateEntry<Input, Result>) => void;
@@ -73,6 +75,8 @@ export declare class MultiCreator<Input, Result> {
73
75
  whenIdle(): Promise<void>;
74
76
  cancel(): void;
75
77
  private run;
78
+ private reject;
79
+ private settle;
76
80
  private replace;
77
81
  private publish;
78
82
  }
@@ -3,6 +3,7 @@
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 { isPreActionBlocked } from "./pre-action.js";
6
7
  import { createEvent, EventBus, notifyObserver, } from "./events.js";
7
8
  export const multiCreateMaximumEntries = 40;
8
9
  export const multiCreateConcurrency = 5;
@@ -138,6 +139,18 @@ export class MultiCreator {
138
139
  async run(pending) {
139
140
  const signal = this.controller.signal;
140
141
  const pool = this.opts.pool ?? sharedMultiCreatePool;
142
+ try {
143
+ signal.throwIfAborted();
144
+ this.opts.before_request?.();
145
+ await this.opts.check?.(pending.filter((entry) => !entry.created_id).map((entry) => entry.input), signal);
146
+ signal.throwIfAborted();
147
+ }
148
+ catch (error) {
149
+ for (const entry of pending)
150
+ this.reject(entry, error);
151
+ this.settle(pending);
152
+ return;
153
+ }
141
154
  await Promise.all(pending.map((entry) => pool.run(async () => {
142
155
  let current = {
143
156
  ...entry,
@@ -175,11 +188,22 @@ export class MultiCreator {
175
188
  });
176
189
  }
177
190
  catch (error) {
178
- current = { ...current, status: MultiCreateStatus.Failed, error };
179
- this.replace(current);
180
- notifyObserver(() => this.opts.on_failed?.(current));
191
+ this.reject(current, error);
181
192
  }
182
193
  })));
194
+ this.settle(pending);
195
+ }
196
+ reject(entry, error) {
197
+ const blocked = isPreActionBlocked(error) && !entry.attempted && !entry.created_id;
198
+ const current = {
199
+ ...entry,
200
+ status: blocked ? MultiCreateStatus.Pending : MultiCreateStatus.Failed,
201
+ error,
202
+ };
203
+ this.replace(current);
204
+ notifyObserver(() => blocked ? this.opts.on_blocked?.(current) : this.opts.on_failed?.(current));
205
+ }
206
+ settle(pending) {
183
207
  this.running = false;
184
208
  this.publish();
185
209
  const ids = new Set(pending.map((entry) => entry.id));
@@ -3,6 +3,9 @@ import { type PaymentsOperator, type BillableOperator, type PayrollsOperator } f
3
3
  import { type SearchOptions, type OwnerSearchOptions, type SearchPage } from "../core/search.js";
4
4
  import { EventBus } from "../core/events.js";
5
5
  import type { MultiCreateAttempt, MultiCreateCheckpoint } from "../core/multi-create.js";
6
+ export declare const PaymentsPreActionReason: {
7
+ readonly RecipientNotOnboarded: "recipient_not_onboarded";
8
+ };
6
9
  type PurchaseCreditsPayload = Parameters<PaymentsOperator["purchaseCredits"]>[0];
7
10
  type OpenPublicSessionPayload = Parameters<PaymentsOperator["openPmtPublicSession"]>[0];
8
11
  type PaymentFromPublicSessionPayload = Parameters<PaymentsOperator["paymentFromPublicSession"]>[0];
@@ -69,6 +72,7 @@ export type PayableEntityBalance = EntityPayments & {
69
72
  export type SearchPayableEntitiesOptions = SearchOptions & {
70
73
  owner_id: string;
71
74
  type: PayableEntityType;
75
+ include_pending?: boolean;
72
76
  };
73
77
  export type ResolvePayableEntityOptions = {
74
78
  owner_id: string;
@@ -191,6 +195,7 @@ export type PaymentsFXOptions = {
191
195
  events?: EventBus;
192
196
  };
193
197
  export interface PaymentsFXOperator {
198
+ checkEntityPaymentRecipients(entities: readonly ResolvePayableEntityOptions[], signal?: AbortSignal): Promise<void>;
194
199
  resolvePayableEntity(opts: ResolvePayableEntityOptions): Promise<PayableEntityBalance>;
195
200
  searchPayableEntities(opts: SearchPayableEntitiesOptions): Promise<SearchPage<PayableEntityBalance>>;
196
201
  createEntityPayment(opts: CreateEntityPaymentOptions, attempt: MultiCreateAttempt): Promise<Payment>;
@@ -243,6 +248,9 @@ export declare class PaymentsFX implements PaymentsFXOperator {
243
248
  resolveEntityPayments(opts: ResolveEntityPaymentsOptions): Promise<EntityPayments[]>;
244
249
  searchPayableEntities(opts: SearchPayableEntitiesOptions): Promise<SearchPage<PayableEntityBalance>>;
245
250
  resolvePayableEntity(opts: ResolvePayableEntityOptions): Promise<PayableEntityBalance>;
251
+ private findPayableEntity;
252
+ checkEntityPaymentRecipients(entities: readonly ResolvePayableEntityOptions[], signal?: AbortSignal): Promise<void>;
253
+ private checkPaymentRecipient;
246
254
  createEntityPayment(opts: CreateEntityPaymentOptions, attempt: MultiCreateAttempt): Promise<Payment>;
247
255
  createPayment(opts: CreatePaymentOptions): Promise<Payment>;
248
256
  replacePayment(opts: ReplacePaymentOptions): Promise<Payment>;
@@ -5,10 +5,14 @@
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
6
  import { CreditLedgerSourceAndroidIAP, CreditLedgerSourceAppleIAP, CreditLedgerSourceStripe, ZeroID, PaymentKindMain, PaymentEntityTypeBill, PaymentEntityTypePayroll, PSPActionableStatusSucceeded, PSPActionableStatusFailed, PSPActionableStatusCanceled, PSPPaymentStatusPending, PSPPaymentStatusPartial, PSPPaymentStatusFull, } from "@fabriktor/schema";
7
7
  import { queryParams, } from "@fabriktor/client";
8
+ import { runPreActionCheck } from "../core/pre-action.js";
8
9
  import { errResolveFailed, errValidation } from "../core/err.js";
9
10
  import { normalizeIDs, requiredOperationID, resolvedID } from "../core/id.js";
10
11
  import { runOwnerSearch, runSearch, } from "../core/search.js";
11
12
  import { createEvent, EventBus } from "../core/events.js";
13
+ export const PaymentsPreActionReason = {
14
+ RecipientNotOnboarded: "recipient_not_onboarded",
15
+ };
12
16
  const entityPaymentQuerySize = 30;
13
17
  const publicAmountPattern = /^\d+$/;
14
18
  const resourcePayment = "payment";
@@ -285,7 +289,8 @@ export class PaymentsFX {
285
289
  entities,
286
290
  signal: opts.signal,
287
291
  });
288
- const results = balances.flatMap((balance, index) => balance.remaining_amount > 0 && !balance.has_pending
292
+ const results = balances.flatMap((balance, index) => balance.remaining_amount > 0 &&
293
+ (opts.include_pending || !balance.has_pending)
289
294
  ? [{ ...balance, entity: entities[index] }]
290
295
  : []);
291
296
  if (results.length || source.done) {
@@ -306,6 +311,21 @@ export class PaymentsFX {
306
311
  }
307
312
  }
308
313
  async resolvePayableEntity(opts) {
314
+ const entity = await this.findPayableEntity(opts);
315
+ const [balance] = await this.resolveEntityPayments({
316
+ owner_id: opts.owner_id,
317
+ type: opts.type,
318
+ entities: [entity],
319
+ signal: opts.signal,
320
+ });
321
+ if (!balance)
322
+ throw errResolveFailed({
323
+ resource: resourcePayment,
324
+ msg: msgPaymentHistoryIncomplete,
325
+ });
326
+ return { ...balance, entity };
327
+ }
328
+ async findPayableEntity(opts) {
309
329
  opts.signal?.throwIfAborted();
310
330
  const record = opts.type === PaymentEntityTypeBill
311
331
  ? (await this.billable?.findOneBill({
@@ -323,19 +343,35 @@ export class PaymentsFX {
323
343
  resource: resourcePayment,
324
344
  msg: msgPaymentMissing,
325
345
  });
326
- const entity = payableEntity(record, opts.type, opts.owner_id);
327
- const [balance] = await this.resolveEntityPayments({
328
- owner_id: opts.owner_id,
329
- type: opts.type,
330
- entities: [entity],
331
- signal: opts.signal,
346
+ return payableEntity(record, opts.type, opts.owner_id);
347
+ }
348
+ // Check the whole selection before starting a batch; never retain readiness across actions.
349
+ async checkEntityPaymentRecipients(entities, signal) {
350
+ const recipients = new Set();
351
+ for (const opts of entities) {
352
+ signal?.throwIfAborted();
353
+ const entity = await this.findPayableEntity({ ...opts, signal });
354
+ recipients.add(entity.recipient_id);
355
+ }
356
+ for (const recipient_id of recipients)
357
+ await this.checkPaymentRecipient(recipient_id, signal);
358
+ }
359
+ async checkPaymentRecipient(recipient_id, signal) {
360
+ if (!resolvedID(recipient_id))
361
+ throw errValidation({ msg: msgPaymentEntityInvalid });
362
+ await runPreActionCheck({
363
+ reason: PaymentsPreActionReason.RecipientNotOnboarded,
364
+ allowed: async () => {
365
+ signal?.throwIfAborted();
366
+ const out = await this.payments.onboardingStatus({ recipient_id, signal });
367
+ if (typeof out.value !== "boolean")
368
+ throw errResolveFailed({
369
+ resource: resourcePmtAccount,
370
+ msg: msgPmtAccountMissing,
371
+ });
372
+ return out.value;
373
+ },
332
374
  });
333
- if (!balance)
334
- throw errResolveFailed({
335
- resource: resourcePayment,
336
- msg: msgPaymentHistoryIncomplete,
337
- });
338
- return { ...balance, entity };
339
375
  }
340
376
  async createEntityPayment(opts, attempt) {
341
377
  if (attempt.created_id)
@@ -385,6 +421,11 @@ export class PaymentsFX {
385
421
  if (opts.created_id)
386
422
  return this.findPayment(opts.created_id, opts.signal);
387
423
  opts.signal?.throwIfAborted();
424
+ if (!opts.in.is_manual &&
425
+ (opts.in.type === PaymentEntityTypeBill ||
426
+ opts.in.type === PaymentEntityTypePayroll))
427
+ await this.checkPaymentRecipient(opts.in.recipient_id, opts.signal);
428
+ opts.signal?.throwIfAborted();
388
429
  opts.before_request?.();
389
430
  const out = await this.payments.insertOnePayment(opts);
390
431
  const id = requiredOperationID(out, msgCreatedPaymentIDMissing);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.132",
3
+ "version": "0.0.134",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,7 +17,7 @@
17
17
  "typescript": "^6.0.3"
18
18
  },
19
19
  "dependencies": {
20
- "@fabriktor/client": "0.0.93",
20
+ "@fabriktor/client": "0.0.94",
21
21
  "@fabriktor/fx": "0.0.94",
22
22
  "@fabriktor/schema": "0.0.66"
23
23
  }