@fabriktor/fx 0.0.135 → 0.0.137

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.
@@ -28,6 +28,7 @@ export type MultiCreateEntry<Input, Result> = {
28
28
  readonly created_id?: string;
29
29
  readonly result?: Result;
30
30
  readonly error?: unknown;
31
+ readonly skipped?: boolean;
31
32
  };
32
33
  export type MultiCreateSnapshot<Input, Result> = {
33
34
  readonly entries: readonly MultiCreateEntry<Input, Result>[];
@@ -48,7 +49,7 @@ export declare class MultiCreatePool {
48
49
  export type MultiCreateOptions<Input, Result> = {
49
50
  run: (input: Input, attempt: MultiCreateAttempt) => Promise<Result>;
50
51
  pool?: MultiCreatePool;
51
- check?: (inputs: readonly Input[], signal: AbortSignal) => Promise<void>;
52
+ check?: (inputs: readonly Input[], signal: AbortSignal) => Promise<void | ReadonlyMap<Input, unknown>>;
52
53
  on_blocked?: (entry: MultiCreateEntry<Input, Result>) => void;
53
54
  retry_same_request?: boolean;
54
55
  before_request?: () => void;
@@ -68,6 +69,7 @@ export declare class MultiCreator<Input, Result> {
68
69
  subscribe(listener: EventListener<MultiCreateSnapshot<Input, Result>>): () => void;
69
70
  add(input: Input): string;
70
71
  edit(id: string, input: Input): boolean;
72
+ skip(id: string, skipped: boolean): boolean;
71
73
  remove(id: string): boolean;
72
74
  start(): Promise<void>;
73
75
  private begin;
@@ -84,7 +84,22 @@ export class MultiCreator {
84
84
  const entry = this.entries.find((entry) => entry.id === id);
85
85
  if (this.running || entry?.status !== MultiCreateStatus.Pending)
86
86
  return false;
87
- this.replace({ ...entry, input: structuredClone(input) });
87
+ this.replace({
88
+ ...entry,
89
+ input: structuredClone(input),
90
+ error: undefined,
91
+ skipped: false,
92
+ });
93
+ return true;
94
+ }
95
+ skip(id, skipped) {
96
+ const entry = this.entries.find((entry) => entry.id === id);
97
+ if (this.running ||
98
+ entry?.status !== MultiCreateStatus.Pending ||
99
+ entry.attempted ||
100
+ entry.created_id)
101
+ return false;
102
+ this.replace({ ...entry, skipped });
88
103
  return true;
89
104
  }
90
105
  remove(id) {
@@ -98,7 +113,7 @@ export class MultiCreator {
98
113
  start() {
99
114
  if (this.running)
100
115
  return this.completion;
101
- const pending = this.entries.filter((entry) => entry.status === MultiCreateStatus.Pending);
116
+ const pending = this.entries.filter((entry) => entry.status === MultiCreateStatus.Pending && !entry.skipped);
102
117
  return this.begin(pending);
103
118
  }
104
119
  begin(pending) {
@@ -142,8 +157,21 @@ export class MultiCreator {
142
157
  try {
143
158
  signal.throwIfAborted();
144
159
  this.opts.before_request?.();
145
- await this.opts.check?.(pending.filter((entry) => !entry.created_id).map((entry) => entry.input), signal);
160
+ const issues = await this.opts.check?.(pending
161
+ .filter((entry) => !entry.created_id)
162
+ .map((entry) => entry.input), signal);
146
163
  signal.throwIfAborted();
164
+ for (const entry of pending) {
165
+ if (issues?.has(entry.input))
166
+ this.reject(entry, issues.get(entry.input));
167
+ else if (entry.error !== undefined)
168
+ this.replace({ ...entry, error: undefined });
169
+ }
170
+ // Leave unaffected entries ready for the user's next selection.
171
+ if (issues?.size) {
172
+ this.settle(pending);
173
+ return;
174
+ }
147
175
  }
148
176
  catch (error) {
149
177
  for (const entry of pending)
@@ -155,6 +183,7 @@ export class MultiCreator {
155
183
  let current = {
156
184
  ...entry,
157
185
  status: MultiCreateStatus.Creating,
186
+ error: undefined,
158
187
  };
159
188
  this.replace(current);
160
189
  try {
@@ -1,11 +1,16 @@
1
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, type SortStripeBalanceResult } from "@fabriktor/schema";
2
2
  import { type PaymentsOperator, type BillableOperator, type PayrollsOperator } from "@fabriktor/client";
3
+ import { PreActionBlocked } from "../core/pre-action.js";
3
4
  import { type SearchOptions, type OwnerSearchOptions, type SearchPage } from "../core/search.js";
4
5
  import { EventBus } from "../core/events.js";
5
6
  import type { MultiCreateAttempt, MultiCreateCheckpoint } from "../core/multi-create.js";
6
7
  export declare const PaymentsPreActionReason: {
7
8
  readonly RecipientNotOnboarded: "recipient_not_onboarded";
8
9
  };
10
+ export declare class PaymentRecipientNotReady extends PreActionBlocked {
11
+ readonly recipient_id: string;
12
+ constructor(recipient_id: string);
13
+ }
9
14
  type PurchaseCreditsPayload = Parameters<PaymentsOperator["purchaseCredits"]>[0];
10
15
  type OpenPublicSessionPayload = Parameters<PaymentsOperator["openPmtPublicSession"]>[0];
11
16
  type PaymentFromPublicSessionPayload = Parameters<PaymentsOperator["paymentFromPublicSession"]>[0];
@@ -195,7 +200,7 @@ export type PaymentsFXOptions = {
195
200
  events?: EventBus;
196
201
  };
197
202
  export interface PaymentsFXOperator {
198
- checkEntityPaymentRecipients(entities: readonly ResolvePayableEntityOptions[], signal?: AbortSignal): Promise<void>;
203
+ checkEntityPaymentRecipients(entities: readonly ResolvePayableEntityOptions[], signal?: AbortSignal): Promise<ReadonlyMap<ResolvePayableEntityOptions, PaymentRecipientNotReady>>;
199
204
  resolvePayableEntity(opts: ResolvePayableEntityOptions): Promise<PayableEntityBalance>;
200
205
  searchPayableEntities(opts: SearchPayableEntitiesOptions): Promise<SearchPage<PayableEntityBalance>>;
201
206
  createEntityPayment(opts: CreateEntityPaymentOptions, attempt: MultiCreateAttempt): Promise<Payment>;
@@ -249,7 +254,7 @@ export declare class PaymentsFX implements PaymentsFXOperator {
249
254
  searchPayableEntities(opts: SearchPayableEntitiesOptions): Promise<SearchPage<PayableEntityBalance>>;
250
255
  resolvePayableEntity(opts: ResolvePayableEntityOptions): Promise<PayableEntityBalance>;
251
256
  private findPayableEntity;
252
- checkEntityPaymentRecipients(entities: readonly ResolvePayableEntityOptions[], signal?: AbortSignal): Promise<void>;
257
+ checkEntityPaymentRecipients(entities: readonly ResolvePayableEntityOptions[], signal?: AbortSignal): Promise<ReadonlyMap<ResolvePayableEntityOptions, PaymentRecipientNotReady>>;
253
258
  private checkPaymentRecipient;
254
259
  createEntityPayment(opts: CreateEntityPaymentOptions, attempt: MultiCreateAttempt): Promise<Payment>;
255
260
  createPayment(opts: CreatePaymentOptions): Promise<Payment>;
@@ -5,7 +5,7 @@
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
+ import { PreActionBlocked, isPreActionBlocked, runPreActionCheck, } from "../core/pre-action.js";
9
9
  import { errResolveFailed, errValidation } from "../core/err.js";
10
10
  import { normalizeIDs, requiredOperationID, resolvedID } from "../core/id.js";
11
11
  import { runOwnerSearch, runSearch, } from "../core/search.js";
@@ -13,6 +13,13 @@ import { createEvent, EventBus } from "../core/events.js";
13
13
  export const PaymentsPreActionReason = {
14
14
  RecipientNotOnboarded: "recipient_not_onboarded",
15
15
  };
16
+ export class PaymentRecipientNotReady extends PreActionBlocked {
17
+ recipient_id;
18
+ constructor(recipient_id) {
19
+ super(PaymentsPreActionReason.RecipientNotOnboarded);
20
+ this.recipient_id = recipient_id;
21
+ }
22
+ }
16
23
  const entityPaymentQuerySize = 30;
17
24
  const publicAmountPattern = /^\d+$/;
18
25
  const resourcePayment = "payment";
@@ -347,31 +354,54 @@ export class PaymentsFX {
347
354
  }
348
355
  // Check the whole selection before starting a batch; never retain readiness across actions.
349
356
  async checkEntityPaymentRecipients(entities, signal) {
350
- const recipients = new Set();
357
+ const recipients = new Map();
358
+ const issues = new Map();
351
359
  for (const opts of entities) {
352
360
  signal?.throwIfAborted();
353
361
  const entity = await this.findPayableEntity({ ...opts, signal });
354
- recipients.add(entity.recipient_id);
362
+ const entries = recipients.get(entity.recipient_id) ?? [];
363
+ entries.push(opts);
364
+ recipients.set(entity.recipient_id, entries);
355
365
  }
356
- for (const recipient_id of recipients)
357
- await this.checkPaymentRecipient(recipient_id, signal);
366
+ for (const [recipient_id, entries] of recipients) {
367
+ try {
368
+ await this.checkPaymentRecipient(recipient_id, signal);
369
+ }
370
+ catch (error) {
371
+ if (!(error instanceof PaymentRecipientNotReady))
372
+ throw error;
373
+ for (const entry of entries)
374
+ issues.set(entry, error);
375
+ }
376
+ }
377
+ return issues;
358
378
  }
359
379
  async checkPaymentRecipient(recipient_id, signal) {
360
380
  if (!resolvedID(recipient_id))
361
381
  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,
382
+ try {
383
+ await runPreActionCheck({
384
+ reason: PaymentsPreActionReason.RecipientNotOnboarded,
385
+ allowed: async () => {
386
+ signal?.throwIfAborted();
387
+ const out = await this.payments.onboardingStatus({
388
+ recipient_id,
389
+ signal,
371
390
  });
372
- return out.value;
373
- },
374
- });
391
+ if (typeof out.value !== "boolean")
392
+ throw errResolveFailed({
393
+ resource: resourcePmtAccount,
394
+ msg: msgPmtAccountMissing,
395
+ });
396
+ return out.value;
397
+ },
398
+ });
399
+ }
400
+ catch (error) {
401
+ if (isPreActionBlocked(error))
402
+ throw new PaymentRecipientNotReady(recipient_id);
403
+ throw error;
404
+ }
375
405
  }
376
406
  async createEntityPayment(opts, attempt) {
377
407
  if (attempt.created_id)
@@ -470,7 +470,7 @@ export class PayrollsWorkspace {
470
470
  source.employee_id === input.employee_id &&
471
471
  source.currency === input.currency, "source");
472
472
  }
473
- await this.adjustments(actor, input.AdjustmentRef, input.currency, signal);
473
+ await this.adjustments(actor, input.adjustment_ref, input.currency, signal);
474
474
  break;
475
475
  }
476
476
  case PayrollResource.EmployeeConfigs:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.135",
3
+ "version": "0.0.137",
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.94",
20
+ "@fabriktor/client": "0.0.95",
21
21
  "@fabriktor/fx": "0.0.94",
22
- "@fabriktor/schema": "0.0.66"
22
+ "@fabriktor/schema": "0.0.67"
23
23
  }
24
24
  }