@fabriktor/fx 0.0.133 → 0.0.135

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];
@@ -192,6 +195,7 @@ export type PaymentsFXOptions = {
192
195
  events?: EventBus;
193
196
  };
194
197
  export interface PaymentsFXOperator {
198
+ checkEntityPaymentRecipients(entities: readonly ResolvePayableEntityOptions[], signal?: AbortSignal): Promise<void>;
195
199
  resolvePayableEntity(opts: ResolvePayableEntityOptions): Promise<PayableEntityBalance>;
196
200
  searchPayableEntities(opts: SearchPayableEntitiesOptions): Promise<SearchPage<PayableEntityBalance>>;
197
201
  createEntityPayment(opts: CreateEntityPaymentOptions, attempt: MultiCreateAttempt): Promise<Payment>;
@@ -244,6 +248,9 @@ export declare class PaymentsFX implements PaymentsFXOperator {
244
248
  resolveEntityPayments(opts: ResolveEntityPaymentsOptions): Promise<EntityPayments[]>;
245
249
  searchPayableEntities(opts: SearchPayableEntitiesOptions): Promise<SearchPage<PayableEntityBalance>>;
246
250
  resolvePayableEntity(opts: ResolvePayableEntityOptions): Promise<PayableEntityBalance>;
251
+ private findPayableEntity;
252
+ checkEntityPaymentRecipients(entities: readonly ResolvePayableEntityOptions[], signal?: AbortSignal): Promise<void>;
253
+ private checkPaymentRecipient;
247
254
  createEntityPayment(opts: CreateEntityPaymentOptions, attempt: MultiCreateAttempt): Promise<Payment>;
248
255
  createPayment(opts: CreatePaymentOptions): Promise<Payment>;
249
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";
@@ -307,6 +311,21 @@ export class PaymentsFX {
307
311
  }
308
312
  }
309
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) {
310
329
  opts.signal?.throwIfAborted();
311
330
  const record = opts.type === PaymentEntityTypeBill
312
331
  ? (await this.billable?.findOneBill({
@@ -324,19 +343,35 @@ export class PaymentsFX {
324
343
  resource: resourcePayment,
325
344
  msg: msgPaymentMissing,
326
345
  });
327
- const entity = payableEntity(record, opts.type, opts.owner_id);
328
- const [balance] = await this.resolveEntityPayments({
329
- owner_id: opts.owner_id,
330
- type: opts.type,
331
- entities: [entity],
332
- 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
+ },
333
374
  });
334
- if (!balance)
335
- throw errResolveFailed({
336
- resource: resourcePayment,
337
- msg: msgPaymentHistoryIncomplete,
338
- });
339
- return { ...balance, entity };
340
375
  }
341
376
  async createEntityPayment(opts, attempt) {
342
377
  if (attempt.created_id)
@@ -386,6 +421,11 @@ export class PaymentsFX {
386
421
  if (opts.created_id)
387
422
  return this.findPayment(opts.created_id, opts.signal);
388
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();
389
429
  opts.before_request?.();
390
430
  const out = await this.payments.insertOnePayment(opts);
391
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.133",
3
+ "version": "0.0.135",
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
  }