@sneat/extension-debtus-ui 0.2.2 → 0.2.3

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.
Files changed (17) hide show
  1. package/fesm2022/{sneat-extension-debtus-ui-new-transfer-page.component-YaGgYqKB.mjs → sneat-extension-debtus-ui-new-transfer-page.component-DUkdGP92.mjs} +10 -4
  2. package/fesm2022/sneat-extension-debtus-ui-new-transfer-page.component-DUkdGP92.mjs.map +1 -0
  3. package/fesm2022/sneat-extension-debtus-ui-source-obligations-page.component-BJ6Oe79f.mjs +992 -0
  4. package/fesm2022/sneat-extension-debtus-ui-source-obligations-page.component-BJ6Oe79f.mjs.map +1 -0
  5. package/fesm2022/sneat-extension-debtus-ui-source-repayment-form-CZ7EtdUp.mjs +30 -0
  6. package/fesm2022/sneat-extension-debtus-ui-source-repayment-form-CZ7EtdUp.mjs.map +1 -0
  7. package/fesm2022/sneat-extension-debtus-ui-transfer-details-page.component-BSDIDPAb.mjs +254 -0
  8. package/fesm2022/sneat-extension-debtus-ui-transfer-details-page.component-BSDIDPAb.mjs.map +1 -0
  9. package/fesm2022/sneat-extension-debtus-ui.mjs +153 -6
  10. package/fesm2022/sneat-extension-debtus-ui.mjs.map +1 -1
  11. package/package.json +7 -4
  12. package/types/sneat-extension-debtus-ui.d.ts +31 -2
  13. package/fesm2022/sneat-extension-debtus-ui-new-transfer-page.component-YaGgYqKB.mjs.map +0 -1
  14. package/fesm2022/sneat-extension-debtus-ui-source-obligations-page.component-6E9_pHCq.mjs +0 -423
  15. package/fesm2022/sneat-extension-debtus-ui-source-obligations-page.component-6E9_pHCq.mjs.map +0 -1
  16. package/fesm2022/sneat-extension-debtus-ui-transfer-details-page.component-B6iQS1YJ.mjs +0 -105
  17. package/fesm2022/sneat-extension-debtus-ui-transfer-details-page.component-B6iQS1YJ.mjs.map +0 -1
@@ -0,0 +1,992 @@
1
+ import { DatePipe } from '@angular/common';
2
+ import { HttpParams, HttpErrorResponse } from '@angular/common/http';
3
+ import * as i0 from '@angular/core';
4
+ import { inject, Injectable, DestroyRef, signal, computed, Component } from '@angular/core';
5
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
+ import { ActivatedRoute } from '@angular/router';
7
+ import { IonBackButton } from '@ionic/angular/ion-back-button';
8
+ import { IonBadge } from '@ionic/angular/ion-badge';
9
+ import { IonButton } from '@ionic/angular/ion-button';
10
+ import { IonButtons } from '@ionic/angular/ion-buttons';
11
+ import { IonCardContent } from '@ionic/angular/ion-card-content';
12
+ import { IonCardHeader } from '@ionic/angular/ion-card-header';
13
+ import { IonCardTitle } from '@ionic/angular/ion-card-title';
14
+ import { IonCard } from '@ionic/angular/ion-card';
15
+ import { IonContent } from '@ionic/angular/ion-content';
16
+ import { IonHeader } from '@ionic/angular/ion-header';
17
+ import { IonInput } from '@ionic/angular/ion-input';
18
+ import { IonItem } from '@ionic/angular/ion-item';
19
+ import { IonLabel } from '@ionic/angular/ion-label';
20
+ import { IonList } from '@ionic/angular/ion-list';
21
+ import { IonNote } from '@ionic/angular/ion-note';
22
+ import { IonSelect } from '@ionic/angular/ion-select';
23
+ import { IonSelectOption } from '@ionic/angular/ion-select-option';
24
+ import { IonSpinner } from '@ionic/angular/ion-spinner';
25
+ import { IonTitle } from '@ionic/angular/ion-title';
26
+ import { IonToolbar } from '@ionic/angular/ion-toolbar';
27
+ import { CONTACTUS_SPACE_SERVICE, contactTitle } from '@sneat/extension-contactus-contract';
28
+ import { parseDebtusSourceObligationDueDateV1, parseDebtusExactMinorAmountString, MAX_DEBTUS_SOURCE_OBLIGATION_IDS, DEBTUS_SOURCE_REPAYMENT_SERVICE_V1, DEBTUS_SOURCE_OBLIGATION_DUE_DATE_SERVICE_V1, DEBTUS_SOURCE_DUE_DATE_CONTRACT_VERSION, DEBTUS_SOURCE_REPAYMENT_CONTRACT_VERSION } from '@sneat/extension-debtus-contract';
29
+ import { LISTUS_SERVICE } from '@sneat/extension-listus-contract';
30
+ import { SpacePageBaseComponent, SpaceComponentBaseParams } from '@sneat/space-components';
31
+ import { ClassName } from '@sneat/ui';
32
+ import { SpaceServiceModule } from '@sneat/space-services';
33
+ import { map, BehaviorSubject, combineLatest, switchMap, of, first, catchError, EMPTY } from 'rxjs';
34
+ import { DEBTUS_SOURCE_RETURN_URL_BUILDER } from './sneat-extension-debtus-ui.mjs';
35
+ import { SneatApiService } from '@sneat/api';
36
+ import { p as parseRepaymentMajorAmount, f as formatMinorAmountForInput } from './sneat-extension-debtus-ui-source-repayment-form-CZ7EtdUp.mjs';
37
+
38
+ class SourceObligationsApiService {
39
+ api = inject(SneatApiService);
40
+ getStatus(source) {
41
+ return this.api
42
+ .get('api4debtus/source-obligations', sourceParams(source))
43
+ .pipe(map((value) => parseSourceObligationsStatus(value, source)));
44
+ }
45
+ getActivities(source, cursor) {
46
+ let params = sourceParams(source).set('pageSize', '25');
47
+ if (cursor) {
48
+ params = params.set('cursor', cursor);
49
+ }
50
+ return this.api
51
+ .get('api4debtus/source-obligation-activities', params)
52
+ .pipe(map((value) => parseSourceObligationActivitiesPage(value, source)));
53
+ }
54
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SourceObligationsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
55
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SourceObligationsApiService, providedIn: 'root' });
56
+ }
57
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SourceObligationsApiService, decorators: [{
58
+ type: Injectable,
59
+ args: [{ providedIn: 'root' }]
60
+ }] });
61
+ function sourceParams(source) {
62
+ return new HttpParams()
63
+ .set('spaceID', source.spaceID)
64
+ .set('sourceNamespace', source.namespace)
65
+ .set('sourceRecordID', source.recordID);
66
+ }
67
+ function parseSourceObligationsStatus(value, expectedSource) {
68
+ const input = record(value, 'source obligations status');
69
+ const source = sourceRef(input['source']);
70
+ if (source.namespace !== expectedSource.namespace ||
71
+ source.spaceID !== expectedSource.spaceID ||
72
+ source.recordID !== expectedSource.recordID) {
73
+ throw new TypeError('source obligations response identity does not match the request');
74
+ }
75
+ const receipt = record(input['latestReceipt'], 'latestReceipt');
76
+ const postingStatus = oneOf(input['postingStatus'], ['pending', 'posting', 'applied', 'attention'], 'postingStatus');
77
+ const attentionReason = optionalString(input['attentionReason'], 'attentionReason');
78
+ if (postingStatus === 'attention' ? !attentionReason : !!attentionReason) {
79
+ throw new TypeError('attentionReason presence does not match postingStatus');
80
+ }
81
+ return {
82
+ source,
83
+ latestReceipt: {
84
+ receiptID: id(inputString(receipt, 'receiptID'), 'latestReceipt.receiptID'),
85
+ revision: positiveIntegerString(receipt['revision'], 'latestReceipt.revision'),
86
+ operationKey: id(inputString(receipt, 'operationKey'), 'latestReceipt.operationKey'),
87
+ inputDigest: id(inputString(receipt, 'inputDigest'), 'latestReceipt.inputDigest'),
88
+ },
89
+ postingStatus,
90
+ ...(attentionReason ? { attentionReason } : {}),
91
+ obligations: boundedArray(input['obligations'], 'obligations').map((obligation) => sourceObligation(obligation, source)),
92
+ };
93
+ }
94
+ function parseSourceObligationActivitiesPage(value, expectedSource) {
95
+ const input = record(value, 'source obligation activities');
96
+ const nextCursor = optionalCursor(input['nextCursor']);
97
+ return {
98
+ activities: boundedArray(input['activities'], 'activities').map((activity) => sourceActivity(activity, expectedSource.spaceID)),
99
+ ...(nextCursor ? { nextCursor } : {}),
100
+ };
101
+ }
102
+ function sourceRef(value) {
103
+ const input = record(value, 'source');
104
+ const namespace = inputString(input, 'namespace');
105
+ if (namespace !== 'splitus') {
106
+ throw new TypeError('source.namespace must be splitus');
107
+ }
108
+ return {
109
+ namespace,
110
+ spaceID: id(inputString(input, 'spaceID'), 'source.spaceID'),
111
+ recordID: id(inputString(input, 'recordID'), 'source.recordID'),
112
+ };
113
+ }
114
+ function sourceContact(value, label, expectedSpaceID) {
115
+ const input = record(value, label);
116
+ const spaceID = id(inputString(input, 'spaceID'), `${label}.spaceID`);
117
+ if (spaceID !== expectedSpaceID) {
118
+ throw new TypeError(`${label}.spaceID does not match the requested Space`);
119
+ }
120
+ return {
121
+ spaceID,
122
+ contactID: id(inputString(input, 'contactID'), `${label}.contactID`),
123
+ };
124
+ }
125
+ function sourceObligation(value, expectedSource) {
126
+ const input = record(value, 'obligation');
127
+ const lineID = id(inputString(input, 'lineID'), 'obligation.lineID');
128
+ const dueDateTask = input['dueDateTask']
129
+ ? parseDebtusSourceObligationDueDateV1(input['dueDateTask'], expectedSource, lineID)
130
+ : undefined;
131
+ return {
132
+ lineID,
133
+ obligationIDs: stringArray(input['obligationIDs'], 'obligation.obligationIDs', MAX_DEBTUS_SOURCE_OBLIGATION_IDS),
134
+ debtor: sourceContact(input['debtor'], 'obligation.debtor', expectedSource.spaceID),
135
+ creditor: sourceContact(input['creditor'], 'obligation.creditor', expectedSource.spaceID),
136
+ currency: currency(input['currency']),
137
+ principalMinor: parseDebtusExactMinorAmountString(input['principalMinor']),
138
+ outstandingMinor: parseDebtusExactMinorAmountString(input['outstandingMinor']),
139
+ repaidMinor: parseDebtusExactMinorAmountString(input['repaidMinor']),
140
+ creditMinor: parseDebtusExactMinorAmountString(input['creditMinor']),
141
+ status: oneOf(input['status'], ['unsettled', 'part_settled', 'settled'], 'obligation.status'),
142
+ repaymentCapability: sourceRepaymentCapability(input['repaymentCapability']),
143
+ ...(dueDateTask ? { dueDateTask } : {}),
144
+ };
145
+ }
146
+ function sourceActivity(value, expectedSpaceID) {
147
+ const input = record(value, 'activity');
148
+ const occurredAt = utcTimestamp(input['occurredAt'], 'activity.occurredAt');
149
+ return {
150
+ activityID: id(inputString(input, 'activityID'), 'activity.activityID'),
151
+ rootActivityID: id(inputString(input, 'rootActivityID'), 'activity.rootActivityID'),
152
+ lineIDs: stringArray(input['lineIDs'], 'activity.lineIDs', MAX_DEBTUS_SOURCE_OBLIGATION_IDS),
153
+ kind: oneOf(input['kind'], [
154
+ 'obligation',
155
+ 'repayment',
156
+ 'adjustment',
157
+ 'cancellation',
158
+ 'credit',
159
+ ], 'activity.kind'),
160
+ from: sourceContact(input['from'], 'activity.from', expectedSpaceID),
161
+ to: sourceContact(input['to'], 'activity.to', expectedSpaceID),
162
+ currency: currency(input['currency']),
163
+ amountMinor: positiveIntegerString(input['amountMinor'], 'activity.amountMinor'),
164
+ occurredAt,
165
+ };
166
+ }
167
+ function utcTimestamp(value, label) {
168
+ const timestamp = text(String(value ?? ''), label);
169
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(timestamp) ||
170
+ Number.isNaN(Date.parse(timestamp))) {
171
+ throw new TypeError(`${label} must be a valid UTC timestamp with millisecond precision`);
172
+ }
173
+ return timestamp;
174
+ }
175
+ function record(value, label) {
176
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
177
+ throw new TypeError(`${label} must be an object`);
178
+ }
179
+ return value;
180
+ }
181
+ function boundedArray(value, label) {
182
+ if (!Array.isArray(value) || value.length > 100) {
183
+ throw new TypeError(`${label} must be an array with at most 100 items`);
184
+ }
185
+ return value;
186
+ }
187
+ function inputString(input, key) {
188
+ const value = input[key];
189
+ if (typeof value !== 'string') {
190
+ throw new TypeError(`${key} must be a string`);
191
+ }
192
+ return value;
193
+ }
194
+ function optionalString(value, label) {
195
+ if (value === undefined) {
196
+ return undefined;
197
+ }
198
+ if (typeof value !== 'string' || !value.trim() || value.length > 2048) {
199
+ throw new TypeError(`${label} must be a bounded non-empty string`);
200
+ }
201
+ return value;
202
+ }
203
+ function optionalCursor(value) {
204
+ if (value === undefined) {
205
+ return undefined;
206
+ }
207
+ if (typeof value !== 'string' || !value || value.length > 4096) {
208
+ throw new TypeError('nextCursor must be a bounded non-empty string');
209
+ }
210
+ return value;
211
+ }
212
+ function text(value, label) {
213
+ if (!value.trim() || value.length > 2048) {
214
+ throw new TypeError(`${label} must be a bounded non-empty string`);
215
+ }
216
+ return value;
217
+ }
218
+ function id(value, label) {
219
+ if (!value ||
220
+ value.trim() !== value ||
221
+ value.length > 512 ||
222
+ Array.from(value).some((character) => {
223
+ const code = character.charCodeAt(0);
224
+ return code < 0x20 || code === 0x7f;
225
+ })) {
226
+ throw new TypeError(`${label} is not a valid bounded identifier`);
227
+ }
228
+ return value;
229
+ }
230
+ function stringArray(value, label, maximum = 100) {
231
+ if (!Array.isArray(value) || value.length > maximum) {
232
+ throw new TypeError(`${label} must be an array with at most ${maximum} items`);
233
+ }
234
+ const values = value.map((item) => {
235
+ if (typeof item !== 'string') {
236
+ throw new TypeError(`${label} entries must be strings`);
237
+ }
238
+ return id(item, label);
239
+ });
240
+ if (!values.length || new Set(values).size !== values.length) {
241
+ throw new TypeError(`${label} must contain unique values`);
242
+ }
243
+ return values;
244
+ }
245
+ function positiveIntegerString(value, label) {
246
+ let result;
247
+ try {
248
+ result = parseDebtusExactMinorAmountString(value);
249
+ }
250
+ catch {
251
+ throw new TypeError(`${label} must be an exact positive integer string`);
252
+ }
253
+ if (result === '0') {
254
+ throw new TypeError(`${label} must be an exact positive integer string`);
255
+ }
256
+ return result;
257
+ }
258
+ function sourceRepaymentCapability(value) {
259
+ const input = record(value, 'repaymentCapability');
260
+ if (input['canRecordRepayment'] === true) {
261
+ if (input['repaymentUnavailableReason'] !== undefined) {
262
+ throw new TypeError('repaymentCapability must not include an unavailable reason when repayment is allowed');
263
+ }
264
+ return { canRecordRepayment: true };
265
+ }
266
+ if (input['canRecordRepayment'] !== false) {
267
+ throw new TypeError('repaymentCapability.canRecordRepayment must be boolean');
268
+ }
269
+ return {
270
+ canRecordRepayment: false,
271
+ repaymentUnavailableReason: oneOf(input['repaymentUnavailableReason'], [
272
+ 'readOnlyRole',
273
+ 'notParty',
274
+ 'notOutstanding',
275
+ 'ledgerPartyUnresolved',
276
+ 'linkedEventLimitReached',
277
+ ], 'repaymentCapability.repaymentUnavailableReason'),
278
+ };
279
+ }
280
+ function currency(value) {
281
+ if (typeof value !== 'string' || !/^[A-Z]{3}$/.test(value)) {
282
+ throw new TypeError('currency must be three uppercase ASCII letters');
283
+ }
284
+ return value;
285
+ }
286
+ function oneOf(value, allowed, label) {
287
+ if (typeof value !== 'string' || !allowed.includes(value)) {
288
+ throw new TypeError(`${label} has an unsupported value`);
289
+ }
290
+ return value;
291
+ }
292
+
293
+ class SourceObligationsPageComponent extends SpacePageBaseComponent {
294
+ sourceApi = inject(SourceObligationsApiService);
295
+ repaymentService = inject(DEBTUS_SOURCE_REPAYMENT_SERVICE_V1);
296
+ dueDateService = inject(DEBTUS_SOURCE_OBLIGATION_DUE_DATE_SERVICE_V1, { optional: true });
297
+ sourceReturnUrlBuilder = inject(DEBTUS_SOURCE_RETURN_URL_BUILDER, { optional: true });
298
+ contactusSpaceService = inject(CONTACTUS_SPACE_SERVICE);
299
+ listusService = inject(LISTUS_SERVICE);
300
+ activatedRoute = inject(ActivatedRoute);
301
+ componentDestroyRef = inject(DestroyRef);
302
+ reload = new BehaviorSubject(undefined);
303
+ $loading = signal(true, /* @ts-ignore */
304
+ ...(ngDevMode ? [{ debugName: "$loading" }] : /* istanbul ignore next */ []));
305
+ $loadingMore = signal(false, /* @ts-ignore */
306
+ ...(ngDevMode ? [{ debugName: "$loadingMore" }] : /* istanbul ignore next */ []));
307
+ $error = signal(undefined, /* @ts-ignore */
308
+ ...(ngDevMode ? [{ debugName: "$error" }] : /* istanbul ignore next */ []));
309
+ $activityError = signal(undefined, /* @ts-ignore */
310
+ ...(ngDevMode ? [{ debugName: "$activityError" }] : /* istanbul ignore next */ []));
311
+ $status = signal(undefined, /* @ts-ignore */
312
+ ...(ngDevMode ? [{ debugName: "$status" }] : /* istanbul ignore next */ []));
313
+ $activities = signal([], /* @ts-ignore */
314
+ ...(ngDevMode ? [{ debugName: "$activities" }] : /* istanbul ignore next */ []));
315
+ $nextCursor = signal(undefined, /* @ts-ignore */
316
+ ...(ngDevMode ? [{ debugName: "$nextCursor" }] : /* istanbul ignore next */ []));
317
+ $requestedLineID = signal(undefined, /* @ts-ignore */
318
+ ...(ngDevMode ? [{ debugName: "$requestedLineID" }] : /* istanbul ignore next */ []));
319
+ $routeSpaceType = signal(undefined, /* @ts-ignore */
320
+ ...(ngDevMode ? [{ debugName: "$routeSpaceType" }] : /* istanbul ignore next */ []));
321
+ $selectedObligation = signal(undefined, /* @ts-ignore */
322
+ ...(ngDevMode ? [{ debugName: "$selectedObligation" }] : /* istanbul ignore next */ []));
323
+ $amountInput = signal('', /* @ts-ignore */
324
+ ...(ngDevMode ? [{ debugName: "$amountInput" }] : /* istanbul ignore next */ []));
325
+ $repaymentPending = signal(false, /* @ts-ignore */
326
+ ...(ngDevMode ? [{ debugName: "$repaymentPending" }] : /* istanbul ignore next */ []));
327
+ $repaymentError = signal(undefined, /* @ts-ignore */
328
+ ...(ngDevMode ? [{ debugName: "$repaymentError" }] : /* istanbul ignore next */ []));
329
+ $repaymentSuccess = signal(undefined, /* @ts-ignore */
330
+ ...(ngDevMode ? [{ debugName: "$repaymentSuccess" }] : /* istanbul ignore next */ []));
331
+ $dueDrafts = signal({}, /* @ts-ignore */
332
+ ...(ngDevMode ? [{ debugName: "$dueDrafts" }] : /* istanbul ignore next */ []));
333
+ $dueLineID = signal(undefined, /* @ts-ignore */
334
+ ...(ngDevMode ? [{ debugName: "$dueLineID" }] : /* istanbul ignore next */ []));
335
+ $todoLists = signal([], /* @ts-ignore */
336
+ ...(ngDevMode ? [{ debugName: "$todoLists" }] : /* istanbul ignore next */ []));
337
+ $duePending = signal(false, /* @ts-ignore */
338
+ ...(ngDevMode ? [{ debugName: "$duePending" }] : /* istanbul ignore next */ []));
339
+ $dueError = signal(undefined, /* @ts-ignore */
340
+ ...(ngDevMode ? [{ debugName: "$dueError" }] : /* istanbul ignore next */ []));
341
+ $dueSuccess = signal(undefined, /* @ts-ignore */
342
+ ...(ngDevMode ? [{ debugName: "$dueSuccess" }] : /* istanbul ignore next */ []));
343
+ $repaymentNeedsReconciliation = signal(false, /* @ts-ignore */
344
+ ...(ngDevMode ? [{ debugName: "$repaymentNeedsReconciliation" }] : /* istanbul ignore next */ []));
345
+ $retryRequest = signal(undefined, /* @ts-ignore */
346
+ ...(ngDevMode ? [{ debugName: "$retryRequest" }] : /* istanbul ignore next */ []));
347
+ routeGeneration = 0;
348
+ $contactTitles = signal(new Map(), /* @ts-ignore */
349
+ ...(ngDevMode ? [{ debugName: "$contactTitles" }] : /* istanbul ignore next */ []));
350
+ $visibleObligations = computed(() => {
351
+ const obligations = this.$status()?.obligations ?? [];
352
+ const requestedLineID = this.$requestedLineID();
353
+ return requestedLineID
354
+ ? obligations.filter((obligation) => obligation.lineID === requestedLineID)
355
+ : obligations;
356
+ }, /* @ts-ignore */
357
+ ...(ngDevMode ? [{ debugName: "$visibleObligations" }] : /* istanbul ignore next */ []));
358
+ $selectionMessage = computed(() => {
359
+ const requestedLineID = this.$requestedLineID();
360
+ if (!requestedLineID) {
361
+ return undefined;
362
+ }
363
+ const count = this.$visibleObligations().length;
364
+ if (count === 0) {
365
+ return 'No Debtus obligation matches the selected bill line.';
366
+ }
367
+ if (count > 1) {
368
+ return `This bill line has ${count} separate Debtus obligations. Choose the exact repayment target.`;
369
+ }
370
+ return 'Showing the obligation for the selected bill line.';
371
+ }, /* @ts-ignore */
372
+ ...(ngDevMode ? [{ debugName: "$selectionMessage" }] : /* istanbul ignore next */ []));
373
+ $amountError = computed(() => {
374
+ const obligation = this.$selectedObligation();
375
+ if (!obligation) {
376
+ return undefined;
377
+ }
378
+ try {
379
+ const amountMinor = parseRepaymentMajorAmount(this.$amountInput(), obligation.currency);
380
+ if (BigInt(amountMinor) > BigInt(obligation.outstandingMinor)) {
381
+ return 'The repayment cannot exceed the outstanding amount.';
382
+ }
383
+ return undefined;
384
+ }
385
+ catch (error) {
386
+ return error instanceof Error ? error.message : 'Enter a valid amount.';
387
+ }
388
+ }, /* @ts-ignore */
389
+ ...(ngDevMode ? [{ debugName: "$amountError" }] : /* istanbul ignore next */ []));
390
+ $sourceReturnUrl = computed(() => {
391
+ const status = this.$status();
392
+ const spaceType = this.$routeSpaceType();
393
+ if (!status || !spaceType || !this.sourceReturnUrlBuilder) {
394
+ return undefined;
395
+ }
396
+ return this.sourceReturnUrlBuilder({
397
+ spaceType,
398
+ source: status.source,
399
+ lineID: this.$requestedLineID(),
400
+ });
401
+ }, /* @ts-ignore */
402
+ ...(ngDevMode ? [{ debugName: "$sourceReturnUrl" }] : /* istanbul ignore next */ []));
403
+ $repaymentMutationLocked = computed(() => this.$repaymentPending() ||
404
+ this.$repaymentNeedsReconciliation() ||
405
+ this.$retryRequest() !== undefined, /* @ts-ignore */
406
+ ...(ngDevMode ? [{ debugName: "$repaymentMutationLocked" }] : /* istanbul ignore next */ []));
407
+ // Cancel must stay usable whenever $repaymentMutationLocked() is true only
408
+ // because a failed attempt left a retry request queued — that is exactly
409
+ // the state Cancel exists to escape. It still blocks while a repayment is
410
+ // genuinely in flight, and while the outcome of a prior attempt is
411
+ // ambiguous ($repaymentNeedsReconciliation) — that state can mean the
412
+ // repayment was actually recorded, so bailing out without reloading could
413
+ // hide a real mismatch; "Reload settlement" is the only safe exit there.
414
+ $cancelRepaymentLocked = computed(() => this.$repaymentPending() || this.$repaymentNeedsReconciliation(), /* @ts-ignore */
415
+ ...(ngDevMode ? [{ debugName: "$cancelRepaymentLocked" }] : /* istanbul ignore next */ []));
416
+ dueDraft(obligation) {
417
+ const saved = obligation.dueDateTask;
418
+ return (this.$dueDrafts()[obligation.lineID] ?? {
419
+ title: saved && 'title' in saved && typeof saved.title === 'string'
420
+ ? saved.title
421
+ : 'Pay shared bill',
422
+ dueDate: saved?.dueDate ?? '',
423
+ todoListID: saved?.todoListID ?? '',
424
+ });
425
+ }
426
+ dueFieldChanged(obligation, field, event) {
427
+ if (this.$duePending())
428
+ return;
429
+ const draft = this.dueDraft(obligation);
430
+ this.$dueDrafts.update((drafts) => ({
431
+ ...drafts,
432
+ [obligation.lineID]: {
433
+ ...draft,
434
+ [field]: String(event.detail.value ?? ''),
435
+ operationKey: undefined,
436
+ },
437
+ }));
438
+ this.clearDueFeedback();
439
+ }
440
+ saveDueDate(obligation) {
441
+ const source = this.$status()?.source;
442
+ const draft = this.dueDraft(obligation);
443
+ const title = draft.title.trim();
444
+ if (!source || this.$duePending() || !title)
445
+ return;
446
+ this.$dueLineID.set(obligation.lineID);
447
+ if (!this.dueDateService) {
448
+ this.$dueError.set('Due date editing is unavailable.');
449
+ return;
450
+ }
451
+ const generation = this.routeGeneration;
452
+ const operationKey = draft.operationKey ?? `web_${globalThis.crypto.randomUUID()}`;
453
+ this.$dueDrafts.update((drafts) => ({
454
+ ...drafts,
455
+ [obligation.lineID]: { ...draft, operationKey },
456
+ }));
457
+ this.$dueLineID.set(obligation.lineID);
458
+ this.$duePending.set(true);
459
+ this.clearDueFeedback();
460
+ this.dueDateService
461
+ .setSourceObligationDueDate({
462
+ contractVersion: DEBTUS_SOURCE_DUE_DATE_CONTRACT_VERSION,
463
+ source,
464
+ lineID: obligation.lineID,
465
+ title,
466
+ dueDate: draft.dueDate || undefined,
467
+ expectedRevision: obligation.dueDateTask?.revision ?? 0,
468
+ operationKey,
469
+ todoListID: draft.todoListID || undefined,
470
+ })
471
+ .pipe(takeUntilDestroyed(this.componentDestroyRef))
472
+ .subscribe({
473
+ next: (dueDateTask) => {
474
+ if (!this.isActiveSource(generation, source))
475
+ return;
476
+ this.$dueDrafts.update((drafts) => {
477
+ const next = { ...drafts };
478
+ delete next[obligation.lineID];
479
+ return next;
480
+ });
481
+ this.$duePending.set(false);
482
+ this.$dueSuccess.set('Due date saved.');
483
+ this.$status.update((status) => status
484
+ ? {
485
+ ...status,
486
+ obligations: status.obligations.map((item) => item.lineID === obligation.lineID
487
+ ? { ...item, dueDateTask }
488
+ : item),
489
+ }
490
+ : status);
491
+ },
492
+ error: (error) => {
493
+ if (!this.isActiveSource(generation, source))
494
+ return;
495
+ this.$duePending.set(false);
496
+ this.$dueError.set('We could not save this due date.');
497
+ this.errorLogger.logError(error, 'Debtus due date could not be saved');
498
+ },
499
+ });
500
+ }
501
+ clearDueFeedback() {
502
+ this.$dueError.set(undefined);
503
+ this.$dueSuccess.set(undefined);
504
+ }
505
+ constructor() {
506
+ super();
507
+ this.$defaultBackUrlSpacePath.set('debts');
508
+ combineLatest([
509
+ this.spaceIDChanged$,
510
+ this.activatedRoute.paramMap,
511
+ this.activatedRoute.queryParamMap,
512
+ this.reload,
513
+ ])
514
+ .pipe(switchMap(([spaceID, pathParams, queryParams]) => {
515
+ this.resetLoadedState();
516
+ const spaceType = pathParams.get('spaceType')?.trim();
517
+ const sourceNamespace = queryParams.get('sourceNamespace');
518
+ const sourceRecordID = queryParams.get('sourceRecordID')?.trim();
519
+ const requestedLineID = queryParams.get('lineID')?.trim();
520
+ if (!spaceID ||
521
+ !spaceType ||
522
+ sourceNamespace !== 'splitus' ||
523
+ !sourceRecordID ||
524
+ !isRouteIdentifier(sourceRecordID) ||
525
+ (requestedLineID && !isRouteIdentifier(requestedLineID))) {
526
+ return of({
527
+ error: new Error('Invalid source-obligations route'),
528
+ message: 'This settlement link is incomplete or invalid.',
529
+ });
530
+ }
531
+ this.$routeSpaceType.set(spaceType);
532
+ this.$requestedLineID.set(requestedLineID || undefined);
533
+ const source = {
534
+ namespace: 'splitus',
535
+ spaceID,
536
+ recordID: sourceRecordID,
537
+ };
538
+ return combineLatest({
539
+ status: this.sourceApi.getStatus(source),
540
+ activityPage: this.sourceApi.getActivities(source),
541
+ contacts: this.contactusSpaceService
542
+ .watchContactBriefs(spaceID)
543
+ .pipe(first()),
544
+ todoLists: this.listusService
545
+ .observeSpaceLists(spaceID)
546
+ .pipe(first()),
547
+ }).pipe(map((data) => ({ data })), catchError((error) => of({
548
+ error,
549
+ message: 'We could not load this bill settlement.',
550
+ })));
551
+ }), takeUntilDestroyed())
552
+ .subscribe((result) => {
553
+ this.$loading.set(false);
554
+ if ('error' in result) {
555
+ this.$error.set(result.message);
556
+ this.errorLogger.logError(result.error, result.message);
557
+ return;
558
+ }
559
+ this.applyLoadedData(result.data);
560
+ this.selectUniqueRequestedObligation(result.data.status);
561
+ });
562
+ }
563
+ retry() {
564
+ if (this.$repaymentPending()) {
565
+ return;
566
+ }
567
+ this.reload.next();
568
+ }
569
+ loadMore() {
570
+ const source = this.$status()?.source;
571
+ const cursor = this.$nextCursor();
572
+ if (!source || !cursor || this.$loadingMore()) {
573
+ return;
574
+ }
575
+ const routeGeneration = this.routeGeneration;
576
+ this.$loadingMore.set(true);
577
+ this.$activityError.set(undefined);
578
+ this.sourceApi
579
+ .getActivities(source, cursor)
580
+ .pipe(takeUntilDestroyed(this.componentDestroyRef))
581
+ .subscribe({
582
+ next: (page) => {
583
+ if (!this.isActiveSource(routeGeneration, source)) {
584
+ return;
585
+ }
586
+ this.$activities.update((activities) => [
587
+ ...activities,
588
+ ...page.activities,
589
+ ]);
590
+ this.$nextCursor.set(page.nextCursor);
591
+ this.$loadingMore.set(false);
592
+ },
593
+ error: (error) => {
594
+ if (!this.isActiveSource(routeGeneration, source)) {
595
+ return;
596
+ }
597
+ this.$activityError.set('We could not load more activity.');
598
+ this.$loadingMore.set(false);
599
+ this.errorLogger.logError(error, 'Failed to load more Debtus activity');
600
+ },
601
+ });
602
+ }
603
+ selectObligation(obligation) {
604
+ if (!this.canStartRepayment(obligation) ||
605
+ this.$repaymentPending() ||
606
+ this.$repaymentNeedsReconciliation()) {
607
+ return;
608
+ }
609
+ const retryRequest = this.$retryRequest();
610
+ if (retryRequest) {
611
+ if (!this.requestMatchesObligation(retryRequest, obligation)) {
612
+ this.requireUncertainRepaymentReconciliation();
613
+ }
614
+ return;
615
+ }
616
+ this.$selectedObligation.set(obligation);
617
+ this.$amountInput.set(formatMinorAmountForInput(obligation.outstandingMinor));
618
+ this.clearRepaymentAttempt();
619
+ }
620
+ cancelRepayment() {
621
+ if (this.$cancelRepaymentLocked()) {
622
+ return;
623
+ }
624
+ this.$selectedObligation.set(undefined);
625
+ this.$amountInput.set('');
626
+ this.clearRepaymentAttempt();
627
+ }
628
+ useFullOutstanding() {
629
+ const obligation = this.$selectedObligation();
630
+ if (!obligation ||
631
+ this.$repaymentPending() ||
632
+ this.$repaymentNeedsReconciliation()) {
633
+ return;
634
+ }
635
+ const retryRequest = this.$retryRequest();
636
+ if (retryRequest) {
637
+ if (retryRequest.amountMinor !== obligation.outstandingMinor) {
638
+ this.requireUncertainRepaymentReconciliation();
639
+ }
640
+ return;
641
+ }
642
+ this.$amountInput.set(formatMinorAmountForInput(obligation.outstandingMinor));
643
+ this.clearRepaymentAttempt();
644
+ }
645
+ amountChanged(event) {
646
+ if (this.$repaymentPending() || this.$repaymentNeedsReconciliation()) {
647
+ return;
648
+ }
649
+ const value = String(event.detail.value ?? '');
650
+ const retryRequest = this.$retryRequest();
651
+ const obligation = this.$selectedObligation();
652
+ if (retryRequest && obligation) {
653
+ try {
654
+ if (parseRepaymentMajorAmount(value, obligation.currency) !==
655
+ retryRequest.amountMinor) {
656
+ this.requireUncertainRepaymentReconciliation();
657
+ return;
658
+ }
659
+ }
660
+ catch {
661
+ this.requireUncertainRepaymentReconciliation();
662
+ return;
663
+ }
664
+ this.$amountInput.set(value);
665
+ return;
666
+ }
667
+ this.$amountInput.set(value);
668
+ this.clearRepaymentAttempt();
669
+ }
670
+ recordRepayment() {
671
+ const obligation = this.$selectedObligation();
672
+ const source = this.$status()?.source;
673
+ const obligationID = obligation && this.rootObligationID(obligation);
674
+ if (!obligation ||
675
+ !source ||
676
+ !obligationID ||
677
+ !this.canStartRepayment(obligation) ||
678
+ this.$repaymentPending() ||
679
+ this.$repaymentNeedsReconciliation()) {
680
+ return;
681
+ }
682
+ let amountMinor;
683
+ try {
684
+ amountMinor = parseRepaymentMajorAmount(this.$amountInput(), obligation.currency);
685
+ if (BigInt(amountMinor) > BigInt(obligation.outstandingMinor)) {
686
+ throw new RangeError('The repayment cannot exceed the outstanding amount.');
687
+ }
688
+ }
689
+ catch (error) {
690
+ this.$repaymentError.set(error instanceof Error ? error.message : 'Enter a valid amount.');
691
+ return;
692
+ }
693
+ const retryRequest = this.$retryRequest();
694
+ if (retryRequest &&
695
+ (!this.requestMatchesObligation(retryRequest, obligation) ||
696
+ !sourceRefsEqual(retryRequest.source, source) ||
697
+ retryRequest.amountMinor !== amountMinor)) {
698
+ this.requireUncertainRepaymentReconciliation();
699
+ return;
700
+ }
701
+ const request = retryRequest ??
702
+ {
703
+ contractVersion: DEBTUS_SOURCE_REPAYMENT_CONTRACT_VERSION,
704
+ source,
705
+ lineID: obligation.lineID,
706
+ obligationID,
707
+ currency: obligation.currency,
708
+ amountMinor,
709
+ repaidAt: new Date().toISOString(),
710
+ operationKey: createRepaymentOperationKey(),
711
+ };
712
+ const routeGeneration = this.routeGeneration;
713
+ this.$retryRequest.set(request);
714
+ this.$repaymentPending.set(true);
715
+ this.$repaymentError.set(undefined);
716
+ this.$repaymentSuccess.set(undefined);
717
+ this.repaymentService
718
+ .recordSourceObligationRepayment(request)
719
+ .pipe(switchMap(() => {
720
+ if (!this.isActiveSource(routeGeneration, source)) {
721
+ return EMPTY;
722
+ }
723
+ // The accepted response is the persisted mutation receipt. A refresh
724
+ // failure must block another mutation until status is reconciled.
725
+ this.$retryRequest.set(undefined);
726
+ this.$selectedObligation.set(undefined);
727
+ this.$amountInput.set('');
728
+ this.$repaymentNeedsReconciliation.set(true);
729
+ return combineLatest({
730
+ status: this.sourceApi.getStatus(source),
731
+ activityPage: this.sourceApi.getActivities(source),
732
+ }).pipe(map((fresh) => fresh), catchError((refreshError) => of({ refreshError })));
733
+ }), takeUntilDestroyed(this.componentDestroyRef))
734
+ .subscribe({
735
+ next: (result) => {
736
+ if (!this.isActiveSource(routeGeneration, source)) {
737
+ return;
738
+ }
739
+ this.$repaymentPending.set(false);
740
+ if ('refreshError' in result) {
741
+ this.$repaymentError.set('The repayment was recorded, but the latest settlement could not be refreshed. Reload this page to see it.');
742
+ this.errorLogger.logError(result.refreshError, 'Debtus repayment refresh failed');
743
+ return;
744
+ }
745
+ this.$status.set(result.status);
746
+ this.$activities.set(result.activityPage.activities);
747
+ this.$nextCursor.set(result.activityPage.nextCursor);
748
+ this.$repaymentNeedsReconciliation.set(false);
749
+ this.$repaymentError.set(undefined);
750
+ this.$repaymentSuccess.set('Repayment recorded.');
751
+ },
752
+ error: (error) => {
753
+ if (!this.isActiveSource(routeGeneration, source)) {
754
+ return;
755
+ }
756
+ this.$repaymentPending.set(false);
757
+ this.$repaymentError.set(repaymentErrorMessage(error));
758
+ this.errorLogger.logError(error, 'Debtus source repayment could not be recorded');
759
+ },
760
+ });
761
+ }
762
+ contactTitle(contactID) {
763
+ return this.$contactTitles().get(contactID) ?? 'Unknown contact';
764
+ }
765
+ formatMinorAmount(minor, currency) {
766
+ if (!['EUR', 'GBP', 'USD'].includes(currency)) {
767
+ return `${minor} minor units ${currency}`;
768
+ }
769
+ return `${currency} ${formatMinorAmountForInput(minor)}`;
770
+ }
771
+ settlementColor(status) {
772
+ switch (status) {
773
+ case 'settled':
774
+ return 'success';
775
+ case 'part_settled':
776
+ return 'warning';
777
+ default:
778
+ return 'medium';
779
+ }
780
+ }
781
+ activityTitle(kind) {
782
+ switch (kind) {
783
+ case 'obligation':
784
+ return 'Obligation recorded';
785
+ case 'repayment':
786
+ return 'Repayment recorded';
787
+ case 'adjustment':
788
+ return 'Adjustment recorded';
789
+ case 'cancellation':
790
+ return 'Obligation cancelled';
791
+ case 'credit':
792
+ return 'Credit recorded';
793
+ }
794
+ }
795
+ obligationTrackKey(obligation) {
796
+ return `${obligation.lineID}:${obligation.obligationIDs.join(',')}`;
797
+ }
798
+ isSelected(obligation) {
799
+ const selected = this.$selectedObligation();
800
+ return selected
801
+ ? this.obligationTrackKey(selected) ===
802
+ this.obligationTrackKey(obligation)
803
+ : false;
804
+ }
805
+ canStartRepayment(obligation) {
806
+ return (obligation.repaymentCapability.canRecordRepayment &&
807
+ obligation.outstandingMinor !== '0' &&
808
+ this.rootObligationID(obligation) !== undefined &&
809
+ ['EUR', 'GBP', 'USD'].includes(obligation.currency));
810
+ }
811
+ repaymentUnavailableMessage(obligation) {
812
+ if (!obligation.repaymentCapability.canRecordRepayment) {
813
+ return capabilityUnavailableMessage(obligation.repaymentCapability.repaymentUnavailableReason);
814
+ }
815
+ if (obligation.obligationIDs.length !== 1) {
816
+ return 'This repayment target has an ambiguous root obligation.';
817
+ }
818
+ if (!['EUR', 'GBP', 'USD'].includes(obligation.currency)) {
819
+ return `Repayments in ${obligation.currency} are not supported yet.`;
820
+ }
821
+ if (obligation.outstandingMinor === '0') {
822
+ return 'This obligation has no outstanding amount.';
823
+ }
824
+ return undefined;
825
+ }
826
+ repaymentButtonLabel() {
827
+ const obligation = this.$selectedObligation();
828
+ if (!obligation || this.$amountError()) {
829
+ return 'Record repayment';
830
+ }
831
+ const amountMinor = parseRepaymentMajorAmount(this.$amountInput(), obligation.currency);
832
+ return amountMinor === obligation.outstandingMinor
833
+ ? 'Record full repayment'
834
+ : 'Record partial repayment';
835
+ }
836
+ resetLoadedState() {
837
+ this.routeGeneration += 1;
838
+ this.$duePending.set(false);
839
+ this.$dueDrafts.set({});
840
+ this.$dueLineID.set(undefined);
841
+ this.clearDueFeedback();
842
+ this.$loading.set(true);
843
+ this.$loadingMore.set(false);
844
+ this.$error.set(undefined);
845
+ this.$activityError.set(undefined);
846
+ this.$status.set(undefined);
847
+ this.$activities.set([]);
848
+ this.$nextCursor.set(undefined);
849
+ this.$requestedLineID.set(undefined);
850
+ this.$routeSpaceType.set(undefined);
851
+ this.$selectedObligation.set(undefined);
852
+ this.$amountInput.set('');
853
+ this.$repaymentPending.set(false);
854
+ this.$repaymentNeedsReconciliation.set(false);
855
+ this.clearRepaymentAttempt();
856
+ }
857
+ clearRepaymentAttempt() {
858
+ this.$retryRequest.set(undefined);
859
+ this.$repaymentError.set(undefined);
860
+ this.$repaymentSuccess.set(undefined);
861
+ }
862
+ requireUncertainRepaymentReconciliation() {
863
+ this.$repaymentError.set('Retry the pending repayment unchanged, or reload the settlement before editing its amount or target.');
864
+ }
865
+ requestMatchesObligation(request, obligation) {
866
+ return (request.lineID === obligation.lineID &&
867
+ request.obligationID === this.rootObligationID(obligation) &&
868
+ request.currency === obligation.currency);
869
+ }
870
+ isActiveSource(routeGeneration, source) {
871
+ return (routeGeneration === this.routeGeneration &&
872
+ sourceRefsEqual(this.$status()?.source, source));
873
+ }
874
+ applyLoadedData(data) {
875
+ this.$status.set(data.status);
876
+ this.$activities.set(data.activityPage.activities);
877
+ this.$nextCursor.set(data.activityPage.nextCursor);
878
+ this.$todoLists.set(Object.entries(data.todoLists)
879
+ .map(([id, brief]) => ({ id, brief }))
880
+ .filter(({ brief }) => brief.type === 'do' && brief.status !== 'archived')
881
+ .sort((a, b) => a.brief.title.localeCompare(b.brief.title)));
882
+ this.$contactTitles.set(new Map(data.contacts.map((contact) => [
883
+ contact.id,
884
+ contactTitle(contact, {
885
+ includeBriefShortTitle: true,
886
+ fallbackToID: false,
887
+ fallback: 'Unknown contact',
888
+ }),
889
+ ])));
890
+ }
891
+ selectUniqueRequestedObligation(status) {
892
+ const requestedLineID = this.$requestedLineID();
893
+ if (!requestedLineID) {
894
+ return;
895
+ }
896
+ const matching = status.obligations.filter((obligation) => obligation.lineID === requestedLineID);
897
+ if (matching.length === 1 && this.canStartRepayment(matching[0])) {
898
+ this.selectObligation(matching[0]);
899
+ }
900
+ }
901
+ rootObligationID(obligation) {
902
+ return obligation.obligationIDs.length === 1
903
+ ? obligation.obligationIDs[0]
904
+ : undefined;
905
+ }
906
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SourceObligationsPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
907
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: SourceObligationsPageComponent, isStandalone: true, selector: "debtus-source-obligations-page", providers: [
908
+ { provide: ClassName, useValue: 'SourceObligationsPageComponent' },
909
+ SpaceComponentBaseParams,
910
+ ], usesInheritance: true, ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-buttons slot=\"start\">\n <ion-back-button [defaultHref]=\"$defaultBackUrl()\" />\n </ion-buttons>\n <ion-title>Bill settlement</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"cardy\">\n @if ($loading()) {\n <div class=\"ion-padding ion-text-center\">\n <ion-spinner name=\"dots\" />\n <p>Loading settlement\u2026</p>\n </div>\n } @else if ($error()) {\n <ion-card>\n <ion-card-content>\n <ion-note color=\"danger\">{{ $error() }}</ion-note>\n <ion-button expand=\"block\" fill=\"outline\" (click)=\"retry()\">\n Try again\n </ion-button>\n </ion-card-content>\n </ion-card>\n } @else if ($status(); as status) {\n <ion-card>\n <ion-card-header>\n <ion-card-title>Splitus bill settlement</ion-card-title>\n </ion-card-header>\n <ion-list lines=\"full\">\n <ion-item>\n <ion-label>\n <p>Posting</p>\n <h2>{{ status.postingStatus }}</h2>\n </ion-label>\n <ion-badge\n slot=\"end\"\n [color]=\"status.postingStatus === 'applied' ? 'success' : 'warning'\"\n >\n {{ status.postingStatus }}\n </ion-badge>\n </ion-item>\n <ion-item>\n <ion-label>\n <p>Debtus receipt</p>\n <h2>Recorded in Debtus</h2>\n <ion-note>Revision {{ status.latestReceipt.revision }}</ion-note>\n </ion-label>\n </ion-item>\n @if (status.attentionReason) {\n <ion-item>\n <ion-label class=\"ion-text-wrap\">\n <p>Needs attention</p>\n <h2>{{ status.attentionReason }}</h2>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n @if ($sourceReturnUrl(); as returnUrl) {\n <ion-card-content>\n <ion-button expand=\"block\" fill=\"outline\" [href]=\"returnUrl\">\n Back to this bill in Splitus\n </ion-button>\n </ion-card-content>\n }\n </ion-card>\n\n <ion-card>\n <ion-card-header>\n <ion-card-title>Who owes what</ion-card-title>\n @if ($selectionMessage(); as message) {\n <ion-note>{{ message }}</ion-note>\n }\n </ion-card-header>\n @if ($visibleObligations().length) {\n <ion-list lines=\"full\">\n @for (\n obligation of $visibleObligations();\n track obligationTrackKey(obligation)\n ) {\n <ion-item\n data-testid=\"debtus-obligation\"\n [attr.data-line-id]=\"obligation.lineID\"\n [attr.data-obligation-id]=\"rootObligationID(obligation)\"\n [color]=\"\n obligation.lineID === $requestedLineID() ? 'light' : undefined\n \"\n >\n <ion-label class=\"ion-text-wrap\">\n <h2>\n {{ contactTitle(obligation.debtor.contactID) }} owes\n {{ contactTitle(obligation.creditor.contactID) }}\n </h2>\n <p>\n Outstanding:\n {{\n formatMinorAmount(\n obligation.outstandingMinor,\n obligation.currency\n )\n }}\n </p>\n <ion-note>\n Original\n {{\n formatMinorAmount(\n obligation.principalMinor,\n obligation.currency\n )\n }}\n \u00B7 Repaid\n {{\n formatMinorAmount(\n obligation.repaidMinor,\n obligation.currency\n )\n }}\n @if (obligation.creditMinor !== '0') {\n \u00B7 Credit\n {{\n formatMinorAmount(\n obligation.creditMinor,\n obligation.currency\n )\n }}\n }\n </ion-note>\n @if (repaymentUnavailableMessage(obligation); as reason) {\n <p>\n <ion-note color=\"medium\">{{ reason }}</ion-note>\n </p>\n }\n <ion-input\n data-testid=\"debtus-due-title-input\"\n label=\"Reminder title\"\n labelPlacement=\"stacked\"\n [value]=\"dueDraft(obligation).title\"\n [disabled]=\"$duePending()\"\n (ionInput)=\"dueFieldChanged(obligation, 'title', $event)\"\n />\n <ion-input\n data-testid=\"debtus-due-date-input\"\n label=\"Due date\"\n labelPlacement=\"stacked\"\n type=\"date\"\n [value]=\"dueDraft(obligation).dueDate\"\n [disabled]=\"$duePending()\"\n (ionInput)=\"dueFieldChanged(obligation, 'dueDate', $event)\"\n />\n <ion-select\n data-testid=\"debtus-todo-list-select\"\n label=\"Also add to a to-do list\"\n labelPlacement=\"stacked\"\n [value]=\"dueDraft(obligation).todoListID\"\n [disabled]=\"$duePending()\"\n (ionChange)=\"\n dueFieldChanged(obligation, 'todoListID', $event)\n \"\n >\n <ion-select-option value=\"\">No to-do list</ion-select-option>\n @for (list of $todoLists(); track list.id) {\n <ion-select-option [value]=\"list.id\">{{\n list.brief.title\n }}</ion-select-option>\n }\n </ion-select>\n @if (\n $dueLineID() === obligation.lineID && $dueError();\n as dueError\n ) {\n <ion-note color=\"danger\">{{ dueError }}</ion-note>\n }\n @if (\n $dueLineID() === obligation.lineID && $dueSuccess();\n as dueSuccess\n ) {\n <ion-note color=\"success\">{{ dueSuccess }}</ion-note>\n }\n <ion-button\n data-testid=\"debtus-save-due-date\"\n size=\"small\"\n [disabled]=\"\n $duePending() || !dueDraft(obligation).title.trim()\n \"\n (click)=\"saveDueDate(obligation)\"\n >Save due date</ion-button\n >\n @if (canStartRepayment(obligation)) {\n <ion-button\n size=\"small\"\n fill=\"outline\"\n [disabled]=\"$repaymentMutationLocked()\"\n (click)=\"selectObligation(obligation)\"\n >\n {{\n isSelected(obligation) ? 'Selected' : 'Record repayment'\n }}\n </ion-button>\n }\n </ion-label>\n <ion-badge\n slot=\"end\"\n [color]=\"settlementColor(obligation.status)\"\n >\n {{\n obligation.status === 'part_settled'\n ? 'part settled'\n : obligation.status\n }}\n </ion-badge>\n </ion-item>\n }\n </ion-list>\n } @else {\n <ion-card-content>\n <ion-note>\n @if ($requestedLineID()) {\n No obligation was recorded for the selected bill line.\n } @else {\n No obligations were recorded for this bill.\n }\n </ion-note>\n </ion-card-content>\n }\n </ion-card>\n\n @if ($selectedObligation(); as obligation) {\n <ion-card>\n <ion-card-header>\n <ion-card-title>Record repayment</ion-card-title>\n <ion-note>\n {{ contactTitle(obligation.debtor.contactID) }} paid\n {{ contactTitle(obligation.creditor.contactID) }}\n </ion-note>\n </ion-card-header>\n <ion-card-content>\n <ion-input\n label=\"Amount\"\n labelPlacement=\"stacked\"\n type=\"text\"\n inputmode=\"decimal\"\n [value]=\"$amountInput()\"\n [disabled]=\"$repaymentMutationLocked()\"\n [helperText]=\"\n 'Available: ' +\n formatMinorAmount(\n obligation.outstandingMinor,\n obligation.currency\n )\n \"\n (ionInput)=\"amountChanged($event)\"\n />\n @if ($amountError(); as amountError) {\n <p>\n <ion-note color=\"danger\">{{ amountError }}</ion-note>\n </p>\n }\n @if ($repaymentError(); as repaymentError) {\n <p>\n <ion-note color=\"danger\">{{ repaymentError }}</ion-note>\n </p>\n }\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n [disabled]=\"$repaymentMutationLocked()\"\n (click)=\"useFullOutstanding()\"\n >\n Use full outstanding amount\n </ion-button>\n <ion-button\n expand=\"block\"\n [disabled]=\"\n $repaymentPending() ||\n $repaymentNeedsReconciliation() ||\n !!$amountError()\n \"\n (click)=\"recordRepayment()\"\n >\n @if ($repaymentPending()) {\n <ion-spinner name=\"dots\" />\n } @else {\n {{ repaymentButtonLabel() }}\n }\n </ion-button>\n <ion-button\n expand=\"block\"\n fill=\"clear\"\n [disabled]=\"$cancelRepaymentLocked()\"\n (click)=\"cancelRepayment()\"\n >\n Cancel\n </ion-button>\n </ion-card-content>\n </ion-card>\n }\n\n @if ($repaymentNeedsReconciliation()) {\n <ion-card>\n <ion-card-content>\n @if ($repaymentPending()) {\n <ion-spinner name=\"dots\" />\n <ion-note> Repayment recorded. Refreshing settlement\u2026 </ion-note>\n } @else {\n <ion-note color=\"danger\">\n {{\n $repaymentError() ||\n 'The repayment was recorded. Reload the settlement before recording another repayment.'\n }}\n </ion-note>\n <ion-button expand=\"block\" fill=\"outline\" (click)=\"retry()\">\n Reload settlement\n </ion-button>\n }\n </ion-card-content>\n </ion-card>\n }\n\n @if ($repaymentSuccess(); as repaymentSuccess) {\n <ion-card>\n <ion-card-content>\n <ion-note color=\"success\">{{ repaymentSuccess }}</ion-note>\n </ion-card-content>\n </ion-card>\n }\n\n <ion-card>\n <ion-card-header>\n <ion-card-title>Activity</ion-card-title>\n </ion-card-header>\n @if ($activities().length) {\n <ion-list lines=\"full\">\n @for (activity of $activities(); track activity.activityID) {\n <ion-item>\n <ion-label class=\"ion-text-wrap\">\n <h2>{{ activityTitle(activity.kind) }}</h2>\n <p>\n {{ contactTitle(activity.from.contactID) }} \u2192\n {{ contactTitle(activity.to.contactID) }}\n </p>\n <ion-note>{{ activity.occurredAt | date: 'medium' }}</ion-note>\n </ion-label>\n <ion-badge slot=\"end\">\n {{ formatMinorAmount(activity.amountMinor, activity.currency) }}\n </ion-badge>\n </ion-item>\n }\n </ion-list>\n } @else {\n <ion-card-content>\n <ion-note>No Debtus activity has been recorded yet.</ion-note>\n </ion-card-content>\n }\n @if ($activityError()) {\n <ion-card-content>\n <ion-note color=\"danger\">{{ $activityError() }}</ion-note>\n </ion-card-content>\n }\n @if ($nextCursor()) {\n <ion-card-content>\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n [disabled]=\"$loadingMore()\"\n (click)=\"loadMore()\"\n >\n @if ($loadingMore()) {\n <ion-spinner name=\"dots\" />\n } @else {\n Load more activity\n }\n </ion-button>\n </ion-card-content>\n }\n </ion-card>\n }\n</ion-content>\n", dependencies: [{ kind: "ngmodule", type: SpaceServiceModule }, { kind: "component", type: IonBackButton, selector: "ion-back-button" }, { kind: "component", type: IonBadge, selector: "ion-badge", inputs: ["color", "mode"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"], outputs: ["ionFocus", "ionBlur"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"], outputs: ["ionScrollStart", "ionScroll", "ionScrollEnd"] }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["description", "disabled", "justify", "labelPlacement", "mode", "value"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "pipe", type: DatePipe, name: "date" }] });
911
+ }
912
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: SourceObligationsPageComponent, decorators: [{
913
+ type: Component,
914
+ args: [{ selector: 'debtus-source-obligations-page', imports: [
915
+ SpaceServiceModule,
916
+ DatePipe,
917
+ IonBackButton,
918
+ IonBadge,
919
+ IonButton,
920
+ IonButtons,
921
+ IonCard,
922
+ IonCardContent,
923
+ IonCardHeader,
924
+ IonCardTitle,
925
+ IonContent,
926
+ IonHeader,
927
+ IonInput,
928
+ IonItem,
929
+ IonLabel,
930
+ IonList,
931
+ IonNote,
932
+ IonSelect,
933
+ IonSelectOption,
934
+ IonSpinner,
935
+ IonTitle,
936
+ IonToolbar,
937
+ ], providers: [
938
+ { provide: ClassName, useValue: 'SourceObligationsPageComponent' },
939
+ SpaceComponentBaseParams,
940
+ ], template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-buttons slot=\"start\">\n <ion-back-button [defaultHref]=\"$defaultBackUrl()\" />\n </ion-buttons>\n <ion-title>Bill settlement</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"cardy\">\n @if ($loading()) {\n <div class=\"ion-padding ion-text-center\">\n <ion-spinner name=\"dots\" />\n <p>Loading settlement\u2026</p>\n </div>\n } @else if ($error()) {\n <ion-card>\n <ion-card-content>\n <ion-note color=\"danger\">{{ $error() }}</ion-note>\n <ion-button expand=\"block\" fill=\"outline\" (click)=\"retry()\">\n Try again\n </ion-button>\n </ion-card-content>\n </ion-card>\n } @else if ($status(); as status) {\n <ion-card>\n <ion-card-header>\n <ion-card-title>Splitus bill settlement</ion-card-title>\n </ion-card-header>\n <ion-list lines=\"full\">\n <ion-item>\n <ion-label>\n <p>Posting</p>\n <h2>{{ status.postingStatus }}</h2>\n </ion-label>\n <ion-badge\n slot=\"end\"\n [color]=\"status.postingStatus === 'applied' ? 'success' : 'warning'\"\n >\n {{ status.postingStatus }}\n </ion-badge>\n </ion-item>\n <ion-item>\n <ion-label>\n <p>Debtus receipt</p>\n <h2>Recorded in Debtus</h2>\n <ion-note>Revision {{ status.latestReceipt.revision }}</ion-note>\n </ion-label>\n </ion-item>\n @if (status.attentionReason) {\n <ion-item>\n <ion-label class=\"ion-text-wrap\">\n <p>Needs attention</p>\n <h2>{{ status.attentionReason }}</h2>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n @if ($sourceReturnUrl(); as returnUrl) {\n <ion-card-content>\n <ion-button expand=\"block\" fill=\"outline\" [href]=\"returnUrl\">\n Back to this bill in Splitus\n </ion-button>\n </ion-card-content>\n }\n </ion-card>\n\n <ion-card>\n <ion-card-header>\n <ion-card-title>Who owes what</ion-card-title>\n @if ($selectionMessage(); as message) {\n <ion-note>{{ message }}</ion-note>\n }\n </ion-card-header>\n @if ($visibleObligations().length) {\n <ion-list lines=\"full\">\n @for (\n obligation of $visibleObligations();\n track obligationTrackKey(obligation)\n ) {\n <ion-item\n data-testid=\"debtus-obligation\"\n [attr.data-line-id]=\"obligation.lineID\"\n [attr.data-obligation-id]=\"rootObligationID(obligation)\"\n [color]=\"\n obligation.lineID === $requestedLineID() ? 'light' : undefined\n \"\n >\n <ion-label class=\"ion-text-wrap\">\n <h2>\n {{ contactTitle(obligation.debtor.contactID) }} owes\n {{ contactTitle(obligation.creditor.contactID) }}\n </h2>\n <p>\n Outstanding:\n {{\n formatMinorAmount(\n obligation.outstandingMinor,\n obligation.currency\n )\n }}\n </p>\n <ion-note>\n Original\n {{\n formatMinorAmount(\n obligation.principalMinor,\n obligation.currency\n )\n }}\n \u00B7 Repaid\n {{\n formatMinorAmount(\n obligation.repaidMinor,\n obligation.currency\n )\n }}\n @if (obligation.creditMinor !== '0') {\n \u00B7 Credit\n {{\n formatMinorAmount(\n obligation.creditMinor,\n obligation.currency\n )\n }}\n }\n </ion-note>\n @if (repaymentUnavailableMessage(obligation); as reason) {\n <p>\n <ion-note color=\"medium\">{{ reason }}</ion-note>\n </p>\n }\n <ion-input\n data-testid=\"debtus-due-title-input\"\n label=\"Reminder title\"\n labelPlacement=\"stacked\"\n [value]=\"dueDraft(obligation).title\"\n [disabled]=\"$duePending()\"\n (ionInput)=\"dueFieldChanged(obligation, 'title', $event)\"\n />\n <ion-input\n data-testid=\"debtus-due-date-input\"\n label=\"Due date\"\n labelPlacement=\"stacked\"\n type=\"date\"\n [value]=\"dueDraft(obligation).dueDate\"\n [disabled]=\"$duePending()\"\n (ionInput)=\"dueFieldChanged(obligation, 'dueDate', $event)\"\n />\n <ion-select\n data-testid=\"debtus-todo-list-select\"\n label=\"Also add to a to-do list\"\n labelPlacement=\"stacked\"\n [value]=\"dueDraft(obligation).todoListID\"\n [disabled]=\"$duePending()\"\n (ionChange)=\"\n dueFieldChanged(obligation, 'todoListID', $event)\n \"\n >\n <ion-select-option value=\"\">No to-do list</ion-select-option>\n @for (list of $todoLists(); track list.id) {\n <ion-select-option [value]=\"list.id\">{{\n list.brief.title\n }}</ion-select-option>\n }\n </ion-select>\n @if (\n $dueLineID() === obligation.lineID && $dueError();\n as dueError\n ) {\n <ion-note color=\"danger\">{{ dueError }}</ion-note>\n }\n @if (\n $dueLineID() === obligation.lineID && $dueSuccess();\n as dueSuccess\n ) {\n <ion-note color=\"success\">{{ dueSuccess }}</ion-note>\n }\n <ion-button\n data-testid=\"debtus-save-due-date\"\n size=\"small\"\n [disabled]=\"\n $duePending() || !dueDraft(obligation).title.trim()\n \"\n (click)=\"saveDueDate(obligation)\"\n >Save due date</ion-button\n >\n @if (canStartRepayment(obligation)) {\n <ion-button\n size=\"small\"\n fill=\"outline\"\n [disabled]=\"$repaymentMutationLocked()\"\n (click)=\"selectObligation(obligation)\"\n >\n {{\n isSelected(obligation) ? 'Selected' : 'Record repayment'\n }}\n </ion-button>\n }\n </ion-label>\n <ion-badge\n slot=\"end\"\n [color]=\"settlementColor(obligation.status)\"\n >\n {{\n obligation.status === 'part_settled'\n ? 'part settled'\n : obligation.status\n }}\n </ion-badge>\n </ion-item>\n }\n </ion-list>\n } @else {\n <ion-card-content>\n <ion-note>\n @if ($requestedLineID()) {\n No obligation was recorded for the selected bill line.\n } @else {\n No obligations were recorded for this bill.\n }\n </ion-note>\n </ion-card-content>\n }\n </ion-card>\n\n @if ($selectedObligation(); as obligation) {\n <ion-card>\n <ion-card-header>\n <ion-card-title>Record repayment</ion-card-title>\n <ion-note>\n {{ contactTitle(obligation.debtor.contactID) }} paid\n {{ contactTitle(obligation.creditor.contactID) }}\n </ion-note>\n </ion-card-header>\n <ion-card-content>\n <ion-input\n label=\"Amount\"\n labelPlacement=\"stacked\"\n type=\"text\"\n inputmode=\"decimal\"\n [value]=\"$amountInput()\"\n [disabled]=\"$repaymentMutationLocked()\"\n [helperText]=\"\n 'Available: ' +\n formatMinorAmount(\n obligation.outstandingMinor,\n obligation.currency\n )\n \"\n (ionInput)=\"amountChanged($event)\"\n />\n @if ($amountError(); as amountError) {\n <p>\n <ion-note color=\"danger\">{{ amountError }}</ion-note>\n </p>\n }\n @if ($repaymentError(); as repaymentError) {\n <p>\n <ion-note color=\"danger\">{{ repaymentError }}</ion-note>\n </p>\n }\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n [disabled]=\"$repaymentMutationLocked()\"\n (click)=\"useFullOutstanding()\"\n >\n Use full outstanding amount\n </ion-button>\n <ion-button\n expand=\"block\"\n [disabled]=\"\n $repaymentPending() ||\n $repaymentNeedsReconciliation() ||\n !!$amountError()\n \"\n (click)=\"recordRepayment()\"\n >\n @if ($repaymentPending()) {\n <ion-spinner name=\"dots\" />\n } @else {\n {{ repaymentButtonLabel() }}\n }\n </ion-button>\n <ion-button\n expand=\"block\"\n fill=\"clear\"\n [disabled]=\"$cancelRepaymentLocked()\"\n (click)=\"cancelRepayment()\"\n >\n Cancel\n </ion-button>\n </ion-card-content>\n </ion-card>\n }\n\n @if ($repaymentNeedsReconciliation()) {\n <ion-card>\n <ion-card-content>\n @if ($repaymentPending()) {\n <ion-spinner name=\"dots\" />\n <ion-note> Repayment recorded. Refreshing settlement\u2026 </ion-note>\n } @else {\n <ion-note color=\"danger\">\n {{\n $repaymentError() ||\n 'The repayment was recorded. Reload the settlement before recording another repayment.'\n }}\n </ion-note>\n <ion-button expand=\"block\" fill=\"outline\" (click)=\"retry()\">\n Reload settlement\n </ion-button>\n }\n </ion-card-content>\n </ion-card>\n }\n\n @if ($repaymentSuccess(); as repaymentSuccess) {\n <ion-card>\n <ion-card-content>\n <ion-note color=\"success\">{{ repaymentSuccess }}</ion-note>\n </ion-card-content>\n </ion-card>\n }\n\n <ion-card>\n <ion-card-header>\n <ion-card-title>Activity</ion-card-title>\n </ion-card-header>\n @if ($activities().length) {\n <ion-list lines=\"full\">\n @for (activity of $activities(); track activity.activityID) {\n <ion-item>\n <ion-label class=\"ion-text-wrap\">\n <h2>{{ activityTitle(activity.kind) }}</h2>\n <p>\n {{ contactTitle(activity.from.contactID) }} \u2192\n {{ contactTitle(activity.to.contactID) }}\n </p>\n <ion-note>{{ activity.occurredAt | date: 'medium' }}</ion-note>\n </ion-label>\n <ion-badge slot=\"end\">\n {{ formatMinorAmount(activity.amountMinor, activity.currency) }}\n </ion-badge>\n </ion-item>\n }\n </ion-list>\n } @else {\n <ion-card-content>\n <ion-note>No Debtus activity has been recorded yet.</ion-note>\n </ion-card-content>\n }\n @if ($activityError()) {\n <ion-card-content>\n <ion-note color=\"danger\">{{ $activityError() }}</ion-note>\n </ion-card-content>\n }\n @if ($nextCursor()) {\n <ion-card-content>\n <ion-button\n expand=\"block\"\n fill=\"outline\"\n [disabled]=\"$loadingMore()\"\n (click)=\"loadMore()\"\n >\n @if ($loadingMore()) {\n <ion-spinner name=\"dots\" />\n } @else {\n Load more activity\n }\n </ion-button>\n </ion-card-content>\n }\n </ion-card>\n }\n</ion-content>\n" }]
941
+ }], ctorParameters: () => [] });
942
+ function createRepaymentOperationKey() {
943
+ return `web_${globalThis.crypto.randomUUID()}`;
944
+ }
945
+ function sourceRefsEqual(first, second) {
946
+ return (first?.namespace === second.namespace &&
947
+ first.spaceID === second.spaceID &&
948
+ first.recordID === second.recordID);
949
+ }
950
+ function isRouteIdentifier(value) {
951
+ return (value.length <= 512 &&
952
+ value.trim() === value &&
953
+ !Array.from(value).some((character) => {
954
+ const code = character.codePointAt(0) ?? 0;
955
+ return code < 0x20 || code === 0x7f;
956
+ }));
957
+ }
958
+ function capabilityUnavailableMessage(reason) {
959
+ switch (reason) {
960
+ case 'readOnlyRole':
961
+ return 'Your Space role can view this obligation but cannot record repayments.';
962
+ case 'notParty':
963
+ return 'You are not a party to this obligation.';
964
+ case 'notOutstanding':
965
+ return 'This obligation is already settled.';
966
+ case 'ledgerPartyUnresolved':
967
+ return 'Debtus could not resolve your ledger party for this obligation.';
968
+ case 'linkedEventLimitReached':
969
+ return 'This obligation has reached its linked repayment limit.';
970
+ }
971
+ }
972
+ function repaymentErrorMessage(error) {
973
+ if (error instanceof HttpErrorResponse) {
974
+ switch (error.status) {
975
+ case 400:
976
+ return 'Debtus rejected the repayment details. Review the amount and try again.';
977
+ case 401:
978
+ return 'Sign in again before recording this repayment.';
979
+ case 403:
980
+ return 'You do not have permission to record this repayment.';
981
+ case 409:
982
+ return 'This obligation changed before the repayment was recorded. Reload the settlement before trying again.';
983
+ }
984
+ }
985
+ if (error instanceof TypeError || error instanceof RangeError) {
986
+ return 'Debtus returned an invalid repayment receipt. Retry uses the same operation key.';
987
+ }
988
+ return 'We could not confirm whether Debtus recorded this repayment. Try again to safely reuse the same operation key.';
989
+ }
990
+
991
+ export { SourceObligationsPageComponent };
992
+ //# sourceMappingURL=sneat-extension-debtus-ui-source-obligations-page.component-BJ6Oe79f.mjs.map