@sneat/extension-debtus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Debtus runtime
2
+
3
+ Private runtime providers for Debtus. It implements the public
4
+ `@sneat/extension-debtus-contract` service contract and is wired only by the
5
+ Debtus application composition root.
@@ -0,0 +1,317 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Injectable } from '@angular/core';
3
+ import { SneatApiService } from '@sneat/api';
4
+ import { debtDirectionToApiDirection, apiDirectionToDebtDirection, DEBTUS_SERVICE } from '@sneat/extension-debtus-contract';
5
+ import { of, throwError, map } from 'rxjs';
6
+
7
+ // ===========================================================================
8
+ // Fable: prototype demo data
9
+ // ---------------------------------------------------------------------------
10
+ // These fixtures back the debtus web-UI flows that do NOT yet have a wired,
11
+ // authenticated Go HTTP endpoint (balances summary, contacts list, per-contact
12
+ // balance, transfer history). They are shaped exactly like the real contract
13
+ // models so that when the backend endpoints are wired (see PR notes:
14
+ // api4unsorted contacts CRUD + a balances endpoint), the internal service can
15
+ // swap `of(demo…)` for a `sneatApiService.get(…)` call with no UI changes.
16
+ //
17
+ // Counterparties are modelled as contactus space contacts (contactID = a
18
+ // contactus contact id) — debtus does NOT own a separate contact list.
19
+ // `bff-friend` is intentionally in a DIFFERENT space to exercise cross-space
20
+ // lending in the UI.
21
+ // ===========================================================================
22
+ const DEMO_CONTACT_BALANCES = [
23
+ {
24
+ contactID: 'contact-alice',
25
+ title: 'Alice Johnson',
26
+ balance: { USD: 120, EUR: 0 },
27
+ },
28
+ {
29
+ contactID: 'contact-bob',
30
+ title: 'Bob Smith',
31
+ balance: { USD: -45 },
32
+ },
33
+ {
34
+ contactID: 'contact-carol',
35
+ title: 'Carol Lee',
36
+ balance: { EUR: 30 },
37
+ },
38
+ {
39
+ // Cross-space counterparty: belongs to their own personal space.
40
+ contactID: 'contact-dave',
41
+ title: 'Dave (family friend)',
42
+ balance: { USD: 200 },
43
+ counterpartySpaceID: 'space-dave-personal',
44
+ },
45
+ {
46
+ contactID: 'contact-erin',
47
+ title: 'Erin Park',
48
+ balance: { EUR: 0, USD: 0 }, // settled up
49
+ },
50
+ ];
51
+ function demoTransfersForSpace(spaceID) {
52
+ return [
53
+ {
54
+ id: 'transfer-1001',
55
+ direction: 'lend',
56
+ amount: { currency: 'USD', value: 120 },
57
+ counterpartyContactID: 'contact-alice',
58
+ counterpartyTitle: 'Alice Johnson',
59
+ note: 'Concert tickets',
60
+ created: '2026-06-20T10:15:00Z',
61
+ dueOn: '2026-07-20T00:00:00Z',
62
+ isReturn: false,
63
+ isOutstanding: true,
64
+ creatorSpaceID: spaceID,
65
+ },
66
+ {
67
+ id: 'transfer-1002',
68
+ direction: 'borrow',
69
+ amount: { currency: 'USD', value: 45 },
70
+ counterpartyContactID: 'contact-bob',
71
+ counterpartyTitle: 'Bob Smith',
72
+ note: 'Lunch',
73
+ created: '2026-06-22T12:30:00Z',
74
+ isReturn: false,
75
+ isOutstanding: true,
76
+ creatorSpaceID: spaceID,
77
+ },
78
+ {
79
+ id: 'transfer-1003',
80
+ direction: 'lend',
81
+ amount: { currency: 'EUR', value: 30 },
82
+ counterpartyContactID: 'contact-carol',
83
+ counterpartyTitle: 'Carol Lee',
84
+ created: '2026-06-25T09:00:00Z',
85
+ isReturn: false,
86
+ isOutstanding: true,
87
+ creatorSpaceID: spaceID,
88
+ },
89
+ {
90
+ id: 'transfer-1004',
91
+ direction: 'lend',
92
+ amount: { currency: 'USD', value: 200 },
93
+ counterpartyContactID: 'contact-dave',
94
+ counterpartyTitle: 'Dave (family friend)',
95
+ note: 'Cross-space loan (different space)',
96
+ created: '2026-06-18T08:00:00Z',
97
+ isReturn: false,
98
+ isOutstanding: true,
99
+ creatorSpaceID: spaceID,
100
+ counterpartySpaceID: 'space-dave-personal',
101
+ },
102
+ {
103
+ id: 'transfer-1005',
104
+ direction: 'borrow',
105
+ amount: { currency: 'USD', value: 20 },
106
+ counterpartyContactID: 'contact-erin',
107
+ counterpartyTitle: 'Erin Park',
108
+ note: 'Coffee (already returned)',
109
+ created: '2026-06-10T08:00:00Z',
110
+ isReturn: false,
111
+ isOutstanding: false,
112
+ creatorSpaceID: spaceID,
113
+ },
114
+ ];
115
+ }
116
+
117
+ class DebtusService {
118
+ sneatApiService = inject(SneatApiService);
119
+ // ----- REAL endpoint: legacy thin create (kept, backwards compatible) -----
120
+ createDebtRecord(request) {
121
+ return this.sneatApiService.post('debtus/create_debt_record', request);
122
+ }
123
+ // ===========================================================================
124
+ // Fable: prototype demo data
125
+ // No wired, authenticated Go HTTP endpoint exists yet for balances / contacts
126
+ // / history (api4unsorted contacts CRUD is defined but not mounted in
127
+ // backend/debtus/module.go; there is no balances endpoint). These read from
128
+ // fixtures. SWAP POINT: replace each `of(...)` with a `sneatApiService.get`
129
+ // once the endpoints are wired — the return types already match.
130
+ // ===========================================================================
131
+ getContactBalances(spaceID) {
132
+ // SWAP: this.sneatApiService.get<IContactBalance[]>('api4debtus/user/contacts', new HttpParams().set('spaceID', spaceID))
133
+ void spaceID;
134
+ return of(DEMO_CONTACT_BALANCES.map((c) => ({ ...c })));
135
+ }
136
+ getContactBalance(spaceID, contactID) {
137
+ void spaceID;
138
+ const found = DEMO_CONTACT_BALANCES.find((c) => c.contactID === contactID) ??
139
+ {
140
+ contactID,
141
+ title: contactID,
142
+ balance: {},
143
+ };
144
+ return of({ ...found });
145
+ }
146
+ getTransfers(spaceID, contactID) {
147
+ // NOTE: GET /api4debtus/user/api4transfers exists but is currently broken
148
+ // server-side (auth bug queries an empty userID). Using fixtures until the
149
+ // backend fix lands. SWAP: this.sneatApiService.get('api4debtus/user/api4transfers', params).
150
+ const all = demoTransfersForSpace(spaceID);
151
+ return of(contactID
152
+ ? all.filter((t) => t.counterpartyContactID === contactID)
153
+ : all);
154
+ }
155
+ // ===========================================================================
156
+ // REAL endpoints below.
157
+ // ===========================================================================
158
+ /** Reads from demo fixtures; unknown ids error (no fabricated receipts). */
159
+ getTransfer(spaceID, transferID) {
160
+ // The live GET transfer endpoint returns a perspective-resolved TransferDto.
161
+ // For the prototype we resolve from fixtures so the receipt screen renders
162
+ // without a live backend; SWAP to the real GET when running against a
163
+ // deployed server:
164
+ // const params = new HttpParams().set('id', transferID);
165
+ // return this.sneatApiService
166
+ // .get<IApiTransferDto>('api4debtus/transfer', params)
167
+ // .pipe(map((dto) => this.mapTransferDto(dto, spaceID)));
168
+ const found = demoTransfersForSpace(spaceID).find((t) => t.id === transferID);
169
+ // Fable refactoring: a transfer that is not in the fixtures (i.e. any
170
+ // REAL transfer just created via POST create-transfer) must be an error,
171
+ // not a fabricated "Unknown / 0.00 USD / Outstanding" receipt — that was
172
+ // presenting fiction as a financial record. The create/settle pages now
173
+ // hand the created transfer to the details page via router state, so this
174
+ // path is only hit on cold loads of unknown ids. The old synthesized
175
+ // fallback is kept below (commented out) per the no-delete policy:
176
+ // return of(
177
+ // found ?? {
178
+ // id: transferID,
179
+ // direction: 'lend',
180
+ // amount: { currency: 'USD', value: 0 },
181
+ // counterpartyContactID: '',
182
+ // counterpartyTitle: 'Unknown',
183
+ // created: new Date().toISOString(),
184
+ // isReturn: false,
185
+ // isOutstanding: true,
186
+ // creatorSpaceID: spaceID,
187
+ // },
188
+ // );
189
+ return found
190
+ ? of(found)
191
+ : throwError(() => new Error(`Transfer "${transferID}" was not found (transfer reads are not wired to the live backend yet).`));
192
+ }
193
+ /** REAL: POST /api4debtus/create-transfer (Firebase-authenticated). */
194
+ createTransfer(request) {
195
+ const apiDirection = debtDirectionToApiDirection(request.direction);
196
+ const body = {
197
+ spaceID: request.spaceID,
198
+ direction: apiDirection,
199
+ amount: {
200
+ currency: request.amount.currency,
201
+ value: request.amount.value,
202
+ },
203
+ // For u2c (lend) the counterparty is the recipient (toContactID); for
204
+ // c2u (borrow) the counterparty is the source (fromContactID).
205
+ toContactID: apiDirection === 'u2c' ? request.contactID : undefined,
206
+ fromContactID: apiDirection === 'c2u' ? request.contactID : undefined,
207
+ // The backend CreateTransferRequest accepts both `note` and
208
+ // `counterpartySpaceID` (facade4debtus/transfers_create_transfer_dto.go);
209
+ // omitting them silently discarded the user's typed note and the
210
+ // cross-space marker on a financial write.
211
+ note: request.note,
212
+ counterpartySpaceID: request.counterpartySpaceID || undefined,
213
+ isReturn: request.isReturn ?? false,
214
+ returnToTransferID: request.returnToTransferID,
215
+ dueOn: request.dueOn,
216
+ };
217
+ return this.sneatApiService
218
+ .post('api4debtus/create-transfer', body)
219
+ .pipe(map((resp) => this.mapCreateResponse(resp, request)));
220
+ }
221
+ /** Settle-up = a reverse-direction return transfer (mirrors the bot). */
222
+ settleUp(request) {
223
+ // Fable refactoring: the direction now comes from the request — the page
224
+ // that shows the balance derives it via `settleDirectionForBalance` and is
225
+ // the source of truth. Previously it was inferred from DEMO_CONTACT_BALANCES
226
+ // fixtures, so any contact NOT in the fixtures got `'borrow'`
227
+ // unconditionally and settling a debt the user owed recorded the WRONG
228
+ // direction, increasing the imbalance. Old fixture-based inference kept
229
+ // below per the no-delete policy:
230
+ // const contact = DEMO_CONTACT_BALANCES.find(
231
+ // (c) => c.contactID === request.contactID,
232
+ // );
233
+ // const currentValue = contact?.balance[request.amount.currency] ?? 0;
234
+ // const direction = settleDirectionForBalance(currentValue || 1);
235
+ const direction = request.direction;
236
+ return this.createTransfer({
237
+ spaceID: request.spaceID,
238
+ direction,
239
+ amount: request.amount,
240
+ contactID: request.contactID,
241
+ contactTitle: request.contactTitle,
242
+ isReturn: true,
243
+ counterpartySpaceID: request.counterpartySpaceID,
244
+ });
245
+ }
246
+ // ----- mapping helpers -----
247
+ mapCreateResponse(resp, request) {
248
+ if (resp.Error) {
249
+ throw new Error(resp.Error);
250
+ }
251
+ const transfer = resp.Transfer
252
+ ? this.mapTransferDto(resp.Transfer, request.spaceID)
253
+ : {
254
+ // If the backend omits the transfer echo, synthesize from the request
255
+ // so the UI can still navigate to a detail screen.
256
+ id: `pending-${Date.now()}`,
257
+ direction: request.direction,
258
+ amount: request.amount,
259
+ counterpartyContactID: request.contactID,
260
+ counterpartyTitle: request.contactTitle ?? request.contactID,
261
+ note: request.note,
262
+ created: new Date().toISOString(),
263
+ dueOn: request.dueOn,
264
+ isReturn: request.isReturn ?? false,
265
+ isOutstanding: true,
266
+ creatorSpaceID: request.spaceID,
267
+ counterpartySpaceID: request.counterpartySpaceID,
268
+ };
269
+ return {
270
+ transfer,
271
+ userBalance: (resp.UserBalance ?? {}),
272
+ counterpartyBalance: (resp.CounterpartyBalance ??
273
+ {}),
274
+ };
275
+ }
276
+ mapTransferDto(dto, spaceID) {
277
+ const counterparty = dto.To ?? dto.From;
278
+ const direction = dto.Direction
279
+ ? apiDirectionToDebtDirection(dto.Direction)
280
+ : 'lend';
281
+ return {
282
+ id: dto.Id,
283
+ direction,
284
+ amount: {
285
+ currency: (dto.Amount?.currency ?? 'USD'),
286
+ value: dto.Amount?.value ?? 0,
287
+ },
288
+ counterpartyContactID: counterparty?.ID ?? '',
289
+ counterpartyTitle: counterparty?.Name ?? 'Unknown',
290
+ note: dto.Comment,
291
+ created: dto.Created ?? new Date().toISOString(),
292
+ dueOn: dto.Due,
293
+ isReturn: dto.IsReturn ?? false,
294
+ isOutstanding: dto.IsOutstanding ?? true,
295
+ creatorSpaceID: spaceID,
296
+ };
297
+ }
298
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: DebtusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
299
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: DebtusService });
300
+ }
301
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: DebtusService, decorators: [{
302
+ type: Injectable
303
+ }] });
304
+
305
+ // Registers the concrete DebtusService and binds it to the DEBTUS_SERVICE token so
306
+ // consumers depend only on the IDebtusService contract. Wired in at app
307
+ // bootstrap (consumers do not import this factory directly).
308
+ function provideDebtus() {
309
+ return [DebtusService, { provide: DEBTUS_SERVICE, useExisting: DebtusService }];
310
+ }
311
+
312
+ /**
313
+ * Generated bundle index. Do not edit.
314
+ */
315
+
316
+ export { DebtusService, provideDebtus };
317
+ //# sourceMappingURL=sneat-extension-debtus.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sneat-extension-debtus.mjs","sources":["../../../../../../libs/extensions/debtus/runtime/src/lib/services/demo-data.ts","../../../../../../libs/extensions/debtus/runtime/src/lib/services/debtus.service.ts","../../../../../../libs/extensions/debtus/runtime/src/lib/provide-debtus.ts","../../../../../../libs/extensions/debtus/runtime/src/sneat-extension-debtus.ts"],"sourcesContent":["import {\n IContactBalance,\n IDebtusTransfer,\n} from '@sneat/extension-debtus-contract';\n\n// ===========================================================================\n// Fable: prototype demo data\n// ---------------------------------------------------------------------------\n// These fixtures back the debtus web-UI flows that do NOT yet have a wired,\n// authenticated Go HTTP endpoint (balances summary, contacts list, per-contact\n// balance, transfer history). They are shaped exactly like the real contract\n// models so that when the backend endpoints are wired (see PR notes:\n// api4unsorted contacts CRUD + a balances endpoint), the internal service can\n// swap `of(demo…)` for a `sneatApiService.get(…)` call with no UI changes.\n//\n// Counterparties are modelled as contactus space contacts (contactID = a\n// contactus contact id) — debtus does NOT own a separate contact list.\n// `bff-friend` is intentionally in a DIFFERENT space to exercise cross-space\n// lending in the UI.\n// ===========================================================================\n\nexport const DEMO_CONTACT_BALANCES: readonly IContactBalance[] = [\n {\n contactID: 'contact-alice',\n title: 'Alice Johnson',\n balance: { USD: 120, EUR: 0 },\n },\n {\n contactID: 'contact-bob',\n title: 'Bob Smith',\n balance: { USD: -45 },\n },\n {\n contactID: 'contact-carol',\n title: 'Carol Lee',\n balance: { EUR: 30 },\n },\n {\n // Cross-space counterparty: belongs to their own personal space.\n contactID: 'contact-dave',\n title: 'Dave (family friend)',\n balance: { USD: 200 },\n counterpartySpaceID: 'space-dave-personal',\n },\n {\n contactID: 'contact-erin',\n title: 'Erin Park',\n balance: { EUR: 0, USD: 0 }, // settled up\n },\n];\n\nexport function demoTransfersForSpace(spaceID: string): IDebtusTransfer[] {\n return [\n {\n id: 'transfer-1001',\n direction: 'lend',\n amount: { currency: 'USD', value: 120 },\n counterpartyContactID: 'contact-alice',\n counterpartyTitle: 'Alice Johnson',\n note: 'Concert tickets',\n created: '2026-06-20T10:15:00Z',\n dueOn: '2026-07-20T00:00:00Z',\n isReturn: false,\n isOutstanding: true,\n creatorSpaceID: spaceID,\n },\n {\n id: 'transfer-1002',\n direction: 'borrow',\n amount: { currency: 'USD', value: 45 },\n counterpartyContactID: 'contact-bob',\n counterpartyTitle: 'Bob Smith',\n note: 'Lunch',\n created: '2026-06-22T12:30:00Z',\n isReturn: false,\n isOutstanding: true,\n creatorSpaceID: spaceID,\n },\n {\n id: 'transfer-1003',\n direction: 'lend',\n amount: { currency: 'EUR', value: 30 },\n counterpartyContactID: 'contact-carol',\n counterpartyTitle: 'Carol Lee',\n created: '2026-06-25T09:00:00Z',\n isReturn: false,\n isOutstanding: true,\n creatorSpaceID: spaceID,\n },\n {\n id: 'transfer-1004',\n direction: 'lend',\n amount: { currency: 'USD', value: 200 },\n counterpartyContactID: 'contact-dave',\n counterpartyTitle: 'Dave (family friend)',\n note: 'Cross-space loan (different space)',\n created: '2026-06-18T08:00:00Z',\n isReturn: false,\n isOutstanding: true,\n creatorSpaceID: spaceID,\n counterpartySpaceID: 'space-dave-personal',\n },\n {\n id: 'transfer-1005',\n direction: 'borrow',\n amount: { currency: 'USD', value: 20 },\n counterpartyContactID: 'contact-erin',\n counterpartyTitle: 'Erin Park',\n note: 'Coffee (already returned)',\n created: '2026-06-10T08:00:00Z',\n isReturn: false,\n isOutstanding: false,\n creatorSpaceID: spaceID,\n },\n ];\n}\n","import { Injectable, inject } from '@angular/core';\nimport { SneatApiService } from '@sneat/api';\nimport {\n IContactBalance,\n ICreateDebtRecordRequest,\n ICreateTransferRequest,\n ICreateTransferResponse,\n IDebtusService,\n IDebtusTransfer,\n ISettleUpRequest,\n apiDirectionToDebtDirection,\n debtDirectionToApiDirection,\n} from '@sneat/extension-debtus-contract';\nimport { Observable, map, of, throwError } from 'rxjs';\nimport { DEMO_CONTACT_BALANCES, demoTransfersForSpace } from './demo-data';\n\n// Backend DTO shapes (facade4debtus/dto4debtus). Only the fields the UI reads\n// are declared here.\ninterface IApiContactDto {\n readonly ID: string;\n readonly UserID?: string;\n readonly Name: string;\n readonly Comment?: string;\n}\n\ninterface IApiTransferDto {\n readonly Id: string;\n readonly Created: string;\n readonly Amount: { readonly currency: string; readonly value: number };\n readonly IsReturn?: boolean;\n readonly CreatorUserID?: string;\n readonly From?: IApiContactDto;\n readonly To?: IApiContactDto;\n readonly Due?: string;\n readonly Direction?: 'u2c' | 'c2u' | '3d-party';\n readonly IsOutstanding?: boolean;\n readonly Comment?: string;\n}\n\ninterface IApiCreateTransferResponse {\n readonly Error?: string;\n readonly Transfer?: IApiTransferDto;\n readonly UserBalance?: Record<string, number>;\n readonly CounterpartyBalance?: Record<string, number>;\n}\n\n@Injectable()\nexport class DebtusService implements IDebtusService {\n private readonly sneatApiService = inject(SneatApiService);\n\n // ----- REAL endpoint: legacy thin create (kept, backwards compatible) -----\n public createDebtRecord(\n request: ICreateDebtRecordRequest,\n ): Observable<string> {\n return this.sneatApiService.post('debtus/create_debt_record', request);\n }\n\n // ===========================================================================\n // Fable: prototype demo data\n // No wired, authenticated Go HTTP endpoint exists yet for balances / contacts\n // / history (api4unsorted contacts CRUD is defined but not mounted in\n // backend/debtus/module.go; there is no balances endpoint). These read from\n // fixtures. SWAP POINT: replace each `of(...)` with a `sneatApiService.get`\n // once the endpoints are wired — the return types already match.\n // ===========================================================================\n\n public getContactBalances(spaceID: string): Observable<IContactBalance[]> {\n // SWAP: this.sneatApiService.get<IContactBalance[]>('api4debtus/user/contacts', new HttpParams().set('spaceID', spaceID))\n void spaceID;\n return of(DEMO_CONTACT_BALANCES.map((c) => ({ ...c })));\n }\n\n public getContactBalance(\n spaceID: string,\n contactID: string,\n ): Observable<IContactBalance> {\n void spaceID;\n const found =\n DEMO_CONTACT_BALANCES.find((c) => c.contactID === contactID) ??\n ({\n contactID,\n title: contactID,\n balance: {},\n } as IContactBalance);\n return of({ ...found });\n }\n\n public getTransfers(\n spaceID: string,\n contactID?: string,\n ): Observable<IDebtusTransfer[]> {\n // NOTE: GET /api4debtus/user/api4transfers exists but is currently broken\n // server-side (auth bug queries an empty userID). Using fixtures until the\n // backend fix lands. SWAP: this.sneatApiService.get('api4debtus/user/api4transfers', params).\n const all = demoTransfersForSpace(spaceID);\n return of(\n contactID\n ? all.filter((t) => t.counterpartyContactID === contactID)\n : all,\n );\n }\n\n // ===========================================================================\n // REAL endpoints below.\n // ===========================================================================\n\n /** Reads from demo fixtures; unknown ids error (no fabricated receipts). */\n public getTransfer(\n spaceID: string,\n transferID: string,\n ): Observable<IDebtusTransfer> {\n // The live GET transfer endpoint returns a perspective-resolved TransferDto.\n // For the prototype we resolve from fixtures so the receipt screen renders\n // without a live backend; SWAP to the real GET when running against a\n // deployed server:\n // const params = new HttpParams().set('id', transferID);\n // return this.sneatApiService\n // .get<IApiTransferDto>('api4debtus/transfer', params)\n // .pipe(map((dto) => this.mapTransferDto(dto, spaceID)));\n const found = demoTransfersForSpace(spaceID).find(\n (t) => t.id === transferID,\n );\n // Fable refactoring: a transfer that is not in the fixtures (i.e. any\n // REAL transfer just created via POST create-transfer) must be an error,\n // not a fabricated \"Unknown / 0.00 USD / Outstanding\" receipt — that was\n // presenting fiction as a financial record. The create/settle pages now\n // hand the created transfer to the details page via router state, so this\n // path is only hit on cold loads of unknown ids. The old synthesized\n // fallback is kept below (commented out) per the no-delete policy:\n // return of(\n // found ?? {\n // id: transferID,\n // direction: 'lend',\n // amount: { currency: 'USD', value: 0 },\n // counterpartyContactID: '',\n // counterpartyTitle: 'Unknown',\n // created: new Date().toISOString(),\n // isReturn: false,\n // isOutstanding: true,\n // creatorSpaceID: spaceID,\n // },\n // );\n return found\n ? of(found)\n : throwError(\n () =>\n new Error(\n `Transfer \"${transferID}\" was not found (transfer reads are not wired to the live backend yet).`,\n ),\n );\n }\n\n /** REAL: POST /api4debtus/create-transfer (Firebase-authenticated). */\n public createTransfer(\n request: ICreateTransferRequest,\n ): Observable<ICreateTransferResponse> {\n const apiDirection = debtDirectionToApiDirection(request.direction);\n const body = {\n spaceID: request.spaceID,\n direction: apiDirection,\n amount: {\n currency: request.amount.currency,\n value: request.amount.value,\n },\n // For u2c (lend) the counterparty is the recipient (toContactID); for\n // c2u (borrow) the counterparty is the source (fromContactID).\n toContactID: apiDirection === 'u2c' ? request.contactID : undefined,\n fromContactID: apiDirection === 'c2u' ? request.contactID : undefined,\n // The backend CreateTransferRequest accepts both `note` and\n // `counterpartySpaceID` (facade4debtus/transfers_create_transfer_dto.go);\n // omitting them silently discarded the user's typed note and the\n // cross-space marker on a financial write.\n note: request.note,\n counterpartySpaceID: request.counterpartySpaceID || undefined,\n isReturn: request.isReturn ?? false,\n returnToTransferID: request.returnToTransferID,\n dueOn: request.dueOn,\n };\n return this.sneatApiService\n .post<IApiCreateTransferResponse>('api4debtus/create-transfer', body)\n .pipe(\n map((resp) => this.mapCreateResponse(resp, request)),\n );\n }\n\n /** Settle-up = a reverse-direction return transfer (mirrors the bot). */\n public settleUp(\n request: ISettleUpRequest,\n ): Observable<ICreateTransferResponse> {\n // Fable refactoring: the direction now comes from the request — the page\n // that shows the balance derives it via `settleDirectionForBalance` and is\n // the source of truth. Previously it was inferred from DEMO_CONTACT_BALANCES\n // fixtures, so any contact NOT in the fixtures got `'borrow'`\n // unconditionally and settling a debt the user owed recorded the WRONG\n // direction, increasing the imbalance. Old fixture-based inference kept\n // below per the no-delete policy:\n // const contact = DEMO_CONTACT_BALANCES.find(\n // (c) => c.contactID === request.contactID,\n // );\n // const currentValue = contact?.balance[request.amount.currency] ?? 0;\n // const direction = settleDirectionForBalance(currentValue || 1);\n const direction = request.direction;\n return this.createTransfer({\n spaceID: request.spaceID,\n direction,\n amount: request.amount,\n contactID: request.contactID,\n contactTitle: request.contactTitle,\n isReturn: true,\n counterpartySpaceID: request.counterpartySpaceID,\n });\n }\n\n // ----- mapping helpers -----\n\n private mapCreateResponse(\n resp: IApiCreateTransferResponse,\n request: ICreateTransferRequest,\n ): ICreateTransferResponse {\n if (resp.Error) {\n throw new Error(resp.Error);\n }\n const transfer: IDebtusTransfer = resp.Transfer\n ? this.mapTransferDto(resp.Transfer, request.spaceID)\n : {\n // If the backend omits the transfer echo, synthesize from the request\n // so the UI can still navigate to a detail screen.\n id: `pending-${Date.now()}`,\n direction: request.direction,\n amount: request.amount,\n counterpartyContactID: request.contactID,\n counterpartyTitle: request.contactTitle ?? request.contactID,\n note: request.note,\n created: new Date().toISOString(),\n dueOn: request.dueOn,\n isReturn: request.isReturn ?? false,\n isOutstanding: true,\n creatorSpaceID: request.spaceID,\n counterpartySpaceID: request.counterpartySpaceID,\n };\n return {\n transfer,\n userBalance: (resp.UserBalance ?? {}) as ICreateTransferResponse['userBalance'],\n counterpartyBalance: (resp.CounterpartyBalance ??\n {}) as ICreateTransferResponse['counterpartyBalance'],\n };\n }\n\n private mapTransferDto(\n dto: IApiTransferDto,\n spaceID: string,\n ): IDebtusTransfer {\n const counterparty = dto.To ?? dto.From;\n const direction = dto.Direction\n ? apiDirectionToDebtDirection(dto.Direction)\n : 'lend';\n return {\n id: dto.Id,\n direction,\n amount: {\n currency: (dto.Amount?.currency ?? 'USD') as\n | 'USD'\n | 'EUR',\n value: dto.Amount?.value ?? 0,\n },\n counterpartyContactID: counterparty?.ID ?? '',\n counterpartyTitle: counterparty?.Name ?? 'Unknown',\n note: dto.Comment,\n created: dto.Created ?? new Date().toISOString(),\n dueOn: dto.Due,\n isReturn: dto.IsReturn ?? false,\n isOutstanding: dto.IsOutstanding ?? true,\n creatorSpaceID: spaceID,\n };\n }\n}\n","import { Provider } from '@angular/core';\nimport { DEBTUS_SERVICE } from '@sneat/extension-debtus-contract';\nimport { DebtusService } from './services';\n\n// Registers the concrete DebtusService and binds it to the DEBTUS_SERVICE token so\n// consumers depend only on the IDebtusService contract. Wired in at app\n// bootstrap (consumers do not import this factory directly).\nexport function provideDebtus(): Provider[] {\n return [DebtusService, { provide: DEBTUS_SERVICE, useExisting: DebtusService }];\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@sneat/extension-debtus",
3
+ "version": "0.1.0",
4
+ "peerDependencies": {
5
+ "@angular/core": ">=22.0.0 <23.0.0",
6
+ "rxjs": "^7.0.0",
7
+ "@sneat/api": "^0.26.4",
8
+ "@sneat/extension-debtus-contract": "^0.2.0"
9
+ },
10
+ "dependencies": {
11
+ "tslib": "^2.3.0"
12
+ },
13
+ "sideEffects": false,
14
+ "module": "fesm2022/sneat-extension-debtus.mjs",
15
+ "typings": "types/sneat-extension-debtus.d.ts",
16
+ "exports": {
17
+ "./package.json": {
18
+ "default": "./package.json"
19
+ },
20
+ ".": {
21
+ "types": "./types/sneat-extension-debtus.d.ts",
22
+ "default": "./fesm2022/sneat-extension-debtus.mjs"
23
+ }
24
+ },
25
+ "type": "module"
26
+ }
@@ -0,0 +1,26 @@
1
+ import { IDebtusService, ICreateDebtRecordRequest, IContactBalance, IDebtusTransfer, ICreateTransferRequest, ICreateTransferResponse, ISettleUpRequest } from '@sneat/extension-debtus-contract';
2
+ import { Observable } from 'rxjs';
3
+ import * as i0 from '@angular/core';
4
+ import { Provider } from '@angular/core';
5
+
6
+ declare class DebtusService implements IDebtusService {
7
+ private readonly sneatApiService;
8
+ createDebtRecord(request: ICreateDebtRecordRequest): Observable<string>;
9
+ getContactBalances(spaceID: string): Observable<IContactBalance[]>;
10
+ getContactBalance(spaceID: string, contactID: string): Observable<IContactBalance>;
11
+ getTransfers(spaceID: string, contactID?: string): Observable<IDebtusTransfer[]>;
12
+ /** Reads from demo fixtures; unknown ids error (no fabricated receipts). */
13
+ getTransfer(spaceID: string, transferID: string): Observable<IDebtusTransfer>;
14
+ /** REAL: POST /api4debtus/create-transfer (Firebase-authenticated). */
15
+ createTransfer(request: ICreateTransferRequest): Observable<ICreateTransferResponse>;
16
+ /** Settle-up = a reverse-direction return transfer (mirrors the bot). */
17
+ settleUp(request: ISettleUpRequest): Observable<ICreateTransferResponse>;
18
+ private mapCreateResponse;
19
+ private mapTransferDto;
20
+ static ɵfac: i0.ɵɵFactoryDeclaration<DebtusService, never>;
21
+ static ɵprov: i0.ɵɵInjectableDeclaration<DebtusService>;
22
+ }
23
+
24
+ declare function provideDebtus(): Provider[];
25
+
26
+ export { DebtusService, provideDebtus };