@africanies/angular-web-sdk 0.1.11 → 0.1.12

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 (93) hide show
  1. package/.nx/cache/{14995846848874973576 → 7821397806208783174}/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +2572 -962
  2. package/.nx/cache/7821397806208783174/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -0
  3. package/.nx/cache/{14995846848874973576 → 7821397806208783174}/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +450 -140
  4. package/.nx/cache/run.json +15 -15
  5. package/.nx/cache/terminalOutputs/16208231681037145050 +85 -0
  6. package/.nx/cache/terminalOutputs/{14995846848874973576 → 7821397806208783174} +2 -2
  7. package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db +0 -0
  8. package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-shm +0 -0
  9. package/.nx/workspace-data/0091ABA9-5C90-580D-953C-8C94E763ADB5-v3.db-wal +0 -0
  10. package/.nx/workspace-data/d/daemon.log +1317 -0
  11. package/.nx/workspace-data/d/server-process.json +2 -2
  12. package/.nx/workspace-data/file-map.json +3458 -3424
  13. package/.nx/workspace-data/nx_files.nxt +0 -0
  14. package/.nx/workspace-data/project-graph.json +2 -2
  15. package/apps/playground/src/app/pages/forms-page.ts +70 -14
  16. package/apps/playground/src/app/snippets/forms.snippets.ts +41 -6
  17. package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +1000 -8
  18. package/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +1 -1
  19. package/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +201 -15
  20. package/libs/africanies-ui/src/index.ts +18 -0
  21. package/libs/africanies-ui/src/lib/forms/index.ts +20 -0
  22. package/libs/africanies-ui/src/lib/forms/phone-input/index.ts +21 -0
  23. package/libs/africanies-ui/src/lib/forms/phone-input/phone-codes.ts +264 -0
  24. package/libs/africanies-ui/src/lib/forms/phone-input/phone-input.component.ts +477 -0
  25. package/libs/africanies-ui/src/lib/forms/phone-input/phone-input.types.ts +35 -0
  26. package/libs/africanies-ui/src/lib/forms/phone-input/phone-input.utils.spec.ts +81 -0
  27. package/libs/africanies-ui/src/lib/forms/phone-input/phone-input.utils.ts +275 -0
  28. package/libs/africanies-ui/src/lib/modules/africanies-forms.module.ts +2 -0
  29. package/libs/africanies-ui/src/lib/navigation/segment/segment.component.ts +2 -1
  30. package/package.json +1 -1
  31. package/.nx/cache/13466509747676776487/dist/libs/africanies-core/README.md +0 -85
  32. package/.nx/cache/13466509747676776487/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +0 -4634
  33. package/.nx/cache/13466509747676776487/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +0 -1
  34. package/.nx/cache/13466509747676776487/dist/libs/africanies-core/package.json +0 -31
  35. package/.nx/cache/13466509747676776487/dist/libs/africanies-core/types/africanies-africanies-core.d.ts +0 -2743
  36. package/.nx/cache/14416694322692463866/dist/libs/africanies-models/README.md +0 -60
  37. package/.nx/cache/14416694322692463866/dist/libs/africanies-models/fesm2022/africanies-africanies-models.mjs +0 -599
  38. package/.nx/cache/14416694322692463866/dist/libs/africanies-models/fesm2022/africanies-africanies-models.mjs.map +0 -1
  39. package/.nx/cache/14416694322692463866/dist/libs/africanies-models/package.json +0 -23
  40. package/.nx/cache/14416694322692463866/dist/libs/africanies-models/types/africanies-africanies-models.d.ts +0 -1452
  41. package/.nx/cache/14876618240301288353/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs +0 -19375
  42. package/.nx/cache/14876618240301288353/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +0 -1
  43. package/.nx/cache/14876618240301288353/dist/libs/africanies-ui/types/africanies-africanies-ui.d.ts +0 -5490
  44. package/.nx/cache/14995846848874973576/dist/libs/africanies-ui/README.md +0 -68
  45. package/.nx/cache/14995846848874973576/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
  46. package/.nx/cache/14995846848874973576/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
  47. package/.nx/cache/14995846848874973576/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +0 -5
  48. package/.nx/cache/14995846848874973576/dist/libs/africanies-ui/assets/carriers/dhl.svg +0 -7
  49. package/.nx/cache/14995846848874973576/dist/libs/africanies-ui/fesm2022/africanies-africanies-ui.mjs.map +0 -1
  50. package/.nx/cache/14995846848874973576/dist/libs/africanies-ui/package.json +0 -35
  51. package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/README.md +0 -46
  52. package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +0 -221
  53. package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +0 -1
  54. package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/package.json +0 -35
  55. package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/tailwind-preset.cjs +0 -253
  56. package/.nx/cache/17626862048742598274/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +0 -166
  57. package/.nx/cache/6664915487114632770/dist/libs/africanies-icons/README.md +0 -69
  58. package/.nx/cache/6664915487114632770/dist/libs/africanies-icons/assets/icons.sprite.svg +0 -1844
  59. package/.nx/cache/6664915487114632770/dist/libs/africanies-icons/fesm2022/africanies-africanies-icons.mjs +0 -832
  60. package/.nx/cache/6664915487114632770/dist/libs/africanies-icons/fesm2022/africanies-africanies-icons.mjs.map +0 -1
  61. package/.nx/cache/6664915487114632770/dist/libs/africanies-icons/package.json +0 -30
  62. package/.nx/cache/6664915487114632770/dist/libs/africanies-icons/types/africanies-africanies-icons.d.ts +0 -108
  63. package/.nx/cache/6673686859704879504/dist/libs/africanies-theme/README.md +0 -46
  64. package/.nx/cache/6673686859704879504/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs +0 -231
  65. package/.nx/cache/6673686859704879504/dist/libs/africanies-theme/fesm2022/africanies-africanies-theme.mjs.map +0 -1
  66. package/.nx/cache/6673686859704879504/dist/libs/africanies-theme/package.json +0 -35
  67. package/.nx/cache/6673686859704879504/dist/libs/africanies-theme/tailwind-preset.cjs +0 -262
  68. package/.nx/cache/6673686859704879504/dist/libs/africanies-theme/types/africanies-africanies-theme.d.ts +0 -174
  69. package/.nx/cache/9040451482998546630/dist/libs/africanies-core/README.md +0 -85
  70. package/.nx/cache/9040451482998546630/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs +0 -4511
  71. package/.nx/cache/9040451482998546630/dist/libs/africanies-core/fesm2022/africanies-africanies-core.mjs.map +0 -1
  72. package/.nx/cache/9040451482998546630/dist/libs/africanies-core/package.json +0 -31
  73. package/.nx/cache/9040451482998546630/dist/libs/africanies-core/types/africanies-africanies-core.d.ts +0 -2741
  74. package/.nx/cache/terminalOutputs/13466509747676776487 +0 -21
  75. package/.nx/cache/terminalOutputs/13555699813138069787 +0 -6
  76. package/.nx/cache/terminalOutputs/14061940909886036014 +0 -173
  77. package/.nx/cache/terminalOutputs/14416694322692463866 +0 -108
  78. package/.nx/cache/terminalOutputs/1446890025183346611 +0 -32
  79. package/.nx/cache/terminalOutputs/14876618240301288353 +0 -21
  80. package/.nx/cache/terminalOutputs/17394769338046050914 +0 -8
  81. package/.nx/cache/terminalOutputs/17626862048742598274 +0 -21
  82. package/.nx/cache/terminalOutputs/6337357866566791975 +0 -6
  83. package/.nx/cache/terminalOutputs/6664915487114632770 +0 -44
  84. package/.nx/cache/terminalOutputs/6673686859704879504 +0 -21
  85. package/.nx/cache/terminalOutputs/783228502946611950 +0 -19
  86. package/.nx/cache/terminalOutputs/7978448102466861029 +0 -32
  87. package/.nx/cache/terminalOutputs/9040451482998546630 +0 -21
  88. /package/.nx/cache/{14876618240301288353 → 7821397806208783174}/dist/libs/africanies-ui/README.md +0 -0
  89. /package/.nx/cache/{14876618240301288353 → 7821397806208783174}/dist/libs/africanies-ui/assets/brand/africanies-logo-mini.png +0 -0
  90. /package/.nx/cache/{14876618240301288353 → 7821397806208783174}/dist/libs/africanies-ui/assets/brand/africanies-logo.png +0 -0
  91. /package/.nx/cache/{14876618240301288353 → 7821397806208783174}/dist/libs/africanies-ui/assets/brand/africanies-logo.svg +0 -0
  92. /package/.nx/cache/{14876618240301288353 → 7821397806208783174}/dist/libs/africanies-ui/assets/carriers/dhl.svg +0 -0
  93. /package/.nx/cache/{14876618240301288353 → 7821397806208783174}/dist/libs/africanies-ui/package.json +0 -0
@@ -1,4511 +0,0 @@
1
- import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Injector, signal, Injectable, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';
3
- import { SessionStorageService, AFRICANIES_SHIPPING_MODE_KEY, STORAGE_TOKEN, AFRICANIES_MODE_CONFIG_KEY, provideLocalStorage, AFRICANIES_ACCESS_TOKEN_KEY } from '@africanies/africanies-storage';
4
- import { of, timeout, throwError, map as map$1, tap as tap$1, catchError, finalize, firstValueFrom, forkJoin } from 'rxjs';
5
- import { HttpErrorResponse, HttpContextToken, HttpContext, HttpClient, HttpHeaders, HttpParams, provideHttpClient, withInterceptors } from '@angular/common/http';
6
- import { DEFAULT_PAGE_SIZE, NOTIFICATION_PAGE_SIZE } from '@africanies/africanies-models';
7
- import { take, tap, map } from 'rxjs/operators';
8
- import { ActivatedRoute, Router } from '@angular/router';
9
-
10
- /**
11
- * DI token for the active {@link AfricaniesSdkConfig}.
12
- *
13
- * Apps must call {@link provideAfricaniesSdk} at bootstrap; injecting without that
14
- * provider throws so misconfiguration fails fast rather than silently using
15
- * an empty base URL.
16
- */
17
- const AFRICANIES_SDK_CONFIG = new InjectionToken('AFRICANIES_SDK_CONFIG');
18
-
19
- const DEFAULT_MODE = 'sfn';
20
- /**
21
- * Signal-based holder for the active {@link ShippingMode}.
22
- *
23
- * Persists through {@link SessionStorageService} under
24
- * {@link AFRICANIES_SHIPPING_MODE_KEY} so each browser tab can hold its own STN/SFN
25
- * context (refresh within the tab keeps the choice; other tabs are unaffected).
26
- * Defaults to `'sfn'` when nothing is stored or the stored value is not a known
27
- * mode — preferring a safe outbound default over failing open on corrupt storage.
28
- *
29
- * Changing mode clears {@link ApiClient}'s GET cache so `readAll` / by-id
30
- * dumps cannot cross STN↔SFN (cache keys omit the mode header). `ApiClient`
31
- * is resolved lazily via {@link Injector} to avoid a DI cycle.
32
- *
33
- * List screens should drop in-memory rows and show a blocking loader until
34
- * the new mode's page arrives — STN rows must not linger on SFN. Use
35
- * `listFetchKind` with `reason: 'mode'`.
36
- *
37
- * Features that must warn before a switch (e.g. mid Create Shipment) can
38
- * {@link registerModeChangeGuard}. The shell switch uses
39
- * {@link requestModeChange}; programmatic {@link setMode} bypasses the guard.
40
- *
41
- * Provided in root; no explicit provider registration is required.
42
- * Pair with {@link shipmentModeInterceptor} so HTTP calls advertise the mode.
43
- *
44
- * @example
45
- * ```ts
46
- * const shipping = inject(ShippingModeService);
47
- * shipping.setMode('stn');
48
- * console.log(shipping.mode()); // 'stn'
49
- * ```
50
- */
51
- class ShippingModeService {
52
- storage = inject(SessionStorageService);
53
- injector = inject(Injector);
54
- _mode = signal(this.readInitialMode(), /* @ts-ignore */
55
- ...(ngDevMode ? [{ debugName: "_mode" }] : /* istanbul ignore next */ []));
56
- modeChangeGuard = null;
57
- /**
58
- * Read-only view of the current shipping mode (`Signal` = Angular's
59
- * readonly signal surface; mutate only via {@link setMode} /
60
- * {@link requestModeChange}).
61
- */
62
- mode = this._mode.asReadonly();
63
- /**
64
- * Register a single guard for shell mode switches. Pass `null` to clear
65
- * (e.g. on feature destroy). Only one guard is active at a time.
66
- * @param guard
67
- */
68
- registerModeChangeGuard(guard) {
69
- this.modeChangeGuard = guard;
70
- }
71
- /**
72
- * Shell switch entry point: runs any registered guard, then applies the
73
- * mode when allowed.
74
- *
75
- * @param mode - `'stn'` or `'sfn'`.
76
- * @returns Emits once with `true` when the mode changed, else `false`.
77
- */
78
- requestModeChange(mode) {
79
- if (mode === this._mode()) {
80
- return of(false);
81
- }
82
- const current = this._mode();
83
- const guard = this.modeChangeGuard;
84
- const allowed$ = guard ? guard(mode, current) : of(true);
85
- return allowed$.pipe(take(1), tap((ok) => {
86
- if (ok) {
87
- this.applyMode(mode);
88
- }
89
- }), map((ok) => !!ok));
90
- }
91
- /**
92
- * Updates the active mode and persists it for the current tab.
93
- * No-ops when `mode` already matches the current value.
94
- * Bypasses {@link registerModeChangeGuard} — prefer
95
- * {@link requestModeChange} for user-driven switches.
96
- *
97
- * @param mode - `'stn'` or `'sfn'`.
98
- */
99
- setMode(mode) {
100
- if (mode === this._mode()) {
101
- return;
102
- }
103
- this.applyMode(mode);
104
- }
105
- applyMode(mode) {
106
- this._mode.set(mode);
107
- this.storage.set(AFRICANIES_SHIPPING_MODE_KEY, mode);
108
- this.injector.get(ApiClient).clearCache();
109
- }
110
- readInitialMode() {
111
- const stored = this.storage.get(AFRICANIES_SHIPPING_MODE_KEY);
112
- return stored === 'stn' || stored === 'sfn' ? stored : DEFAULT_MODE;
113
- }
114
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ShippingModeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
115
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ShippingModeService, providedIn: 'root' });
116
- }
117
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ShippingModeService, decorators: [{
118
- type: Injectable,
119
- args: [{ providedIn: 'root' }]
120
- }] });
121
-
122
- /**
123
- * Null-safe coercions for AFRICANIES wire JSON.
124
- *
125
- * Mappers should run every request/response field through these helpers so
126
- * missing keys, `null`, and wrong JSON types never throw at `.map` / `.trim`.
127
- */
128
- /**
129
- * Narrow unknown JSON into a plain object record.
130
- * @param value - Candidate value.
131
- * @returns Record when a non-array object; otherwise `null`.
132
- */
133
- function asRecord(value) {
134
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
135
- return value;
136
- }
137
- return null;
138
- }
139
- /**
140
- * Coerce a value to an array. Non-arrays become `[]`.
141
- * @param value - Candidate list.
142
- */
143
- function asArray(value) {
144
- return Array.isArray(value) ? value : [];
145
- }
146
- /**
147
- * Map every element of a wire list. Non-arrays yield `[]`.
148
- * @param value - Candidate list.
149
- * @param mapOne - Per-item mapper.
150
- */
151
- function mapArray(value, mapOne) {
152
- return asArray(value).map(mapOne);
153
- }
154
- /**
155
- * If a list payload is wrapped in a Laravel paginator / `{ data | items }`,
156
- * return the inner array. Otherwise return `value` unchanged.
157
- * @param value - Envelope `data` or a nested list field.
158
- */
159
- function unwrapListPayload(value) {
160
- if (Array.isArray(value) || value == null) {
161
- return value;
162
- }
163
- const record = asRecord(value);
164
- if (record === null) {
165
- return value;
166
- }
167
- const nested = record['data'] ?? record['items'] ?? record['results'];
168
- if (Array.isArray(nested)) {
169
- return nested;
170
- }
171
- if (nested == null &&
172
- ('current_page' in record ||
173
- 'currentPage' in record ||
174
- 'last_page' in record ||
175
- 'lastPage' in record ||
176
- 'per_page' in record ||
177
- 'perPage' in record)) {
178
- return [];
179
- }
180
- return value;
181
- }
182
- /**
183
- * Map a list-or-single payload (Laravel sometimes sends one object).
184
- *
185
- * - `null` / `undefined` → `[]`
186
- * - paginator / `{ data | items }` → mapped inner rows
187
- * - array → mapped items
188
- * - anything else → one mapped item
189
- *
190
- * @param value - Envelope `data` (or a nested list field).
191
- * @param mapOne - Per-item mapper.
192
- */
193
- function mapList(value, mapOne) {
194
- const list = unwrapListPayload(value);
195
- if (list == null) {
196
- return [];
197
- }
198
- if (Array.isArray(list)) {
199
- return list.map(mapOne);
200
- }
201
- return [mapOne(list)];
202
- }
203
- /**
204
- * Finite number, or `fallback` when missing/invalid.
205
- * @param value - Raw numeric field.
206
- * @param fallback - Default when not a finite number.
207
- */
208
- function asNumber(value, fallback = 0) {
209
- const n = Number(value);
210
- return Number.isFinite(n) ? n : fallback;
211
- }
212
- /**
213
- * Finite number, or `null` when missing/invalid.
214
- * @param value - Raw numeric field.
215
- */
216
- function asNullableNumber(value) {
217
- if (value == null || value === '') {
218
- return null;
219
- }
220
- const n = Number(value);
221
- return Number.isFinite(n) ? n : null;
222
- }
223
- /**
224
- * String, or `fallback` when the value cannot be stringified usefully.
225
- * @param value - Raw string field.
226
- * @param fallback - Default when nullish / non-scalar.
227
- */
228
- function asString(value, fallback = '') {
229
- if (value == null) {
230
- return fallback;
231
- }
232
- if (typeof value === 'string') {
233
- return value;
234
- }
235
- if (typeof value === 'number' || typeof value === 'boolean') {
236
- return String(value);
237
- }
238
- return fallback;
239
- }
240
- /**
241
- * String or `null` (never `undefined`).
242
- * @param value - Raw string field.
243
- */
244
- function asNullableString(value) {
245
- if (value == null) {
246
- return null;
247
- }
248
- if (typeof value === 'string') {
249
- return value;
250
- }
251
- if (typeof value === 'number' || typeof value === 'boolean') {
252
- return String(value);
253
- }
254
- return null;
255
- }
256
- /**
257
- * Boolean from wire flags (`true` / `false` / `1` / `0` / `"true"` / `"1"`).
258
- * Missing values are `false`.
259
- * @param value - Raw flag.
260
- */
261
- function asBoolean(value) {
262
- if (typeof value === 'boolean') {
263
- return value;
264
- }
265
- if (typeof value === 'number') {
266
- return value === 1;
267
- }
268
- const trimmed = String(value ?? '')
269
- .trim()
270
- .toLowerCase();
271
- return trimmed === '1' || trimmed === 'true';
272
- }
273
- /**
274
- * Boolean or `null` when the field is absent / unparseable.
275
- * @param value - Raw flag.
276
- */
277
- function asNullableBoolean(value) {
278
- if (value == null || value === '') {
279
- return null;
280
- }
281
- if (typeof value === 'boolean') {
282
- return value;
283
- }
284
- if (typeof value === 'number') {
285
- return value === 1;
286
- }
287
- if (typeof value === 'string') {
288
- const trimmed = value.trim().toLowerCase();
289
- if (trimmed === '1' || trimmed === 'true') {
290
- return true;
291
- }
292
- if (trimmed === '0' || trimmed === 'false') {
293
- return false;
294
- }
295
- }
296
- return null;
297
- }
298
- /**
299
- * Wire `"1"` / `"0"` from a boolean, number, or string flag.
300
- * @param value - Host or wire flag.
301
- */
302
- function toFlag01(value) {
303
- return asBoolean(value) ? '1' : '0';
304
- }
305
- /**
306
- * User-model `0` / `1` flag, or `null` when absent.
307
- * @param value - Raw flag.
308
- */
309
- function asNullableFlag01(value) {
310
- if (value == null || value === '') {
311
- return null;
312
- }
313
- if (value === 1 || value === '1' || value === true) {
314
- return 1;
315
- }
316
- if (value === 0 || value === '0' || value === false) {
317
- return 0;
318
- }
319
- return null;
320
- }
321
-
322
- /**
323
- * Laravel-style validation bag: `{ field: ["msg", ...] }`.
324
- * @param value - Candidate JSON object.
325
- */
326
- function isLaravelValidationBag(value) {
327
- const record = asRecord(value);
328
- if (record === null) {
329
- return false;
330
- }
331
- const keys = Object.keys(record);
332
- if (keys.length === 0) {
333
- return false;
334
- }
335
- return keys.every((key) => {
336
- const entry = record[key];
337
- return (Array.isArray(entry) &&
338
- entry.every((item) => typeof item === 'string'));
339
- });
340
- }
341
- /**
342
- * Flatten a validation bag into {@link ApiErrorDetailModel} rows.
343
- *
344
- * Uses messages in order; the first message per field is what
345
- * {@link fieldErrorsMap} exposes for form binding.
346
- * @param bag - Field → messages map.
347
- */
348
- function mapLaravelValidationBag(bag) {
349
- const details = [];
350
- for (const [field, rawMessages] of Object.entries(bag)) {
351
- const messages = asArray(rawMessages)
352
- .map((m) => asString(m).trim())
353
- .filter((m) => m.length > 0);
354
- for (const message of messages) {
355
- details.push({ field, message, code: null });
356
- }
357
- }
358
- return details;
359
- }
360
- /**
361
- * Join field errors for toast / banner copy (one line per message, de-duped).
362
- * Prefer this over top-level `message` when a bag is present — the envelope
363
- * `message` is often only the first field.
364
- * @param errors - Normalized field errors.
365
- */
366
- function joinApiErrorMessages(errors) {
367
- if (!errors?.length) {
368
- return null;
369
- }
370
- const lines = [];
371
- const seen = new Set();
372
- for (const detail of errors) {
373
- const message = detail.message.trim();
374
- if (!message || seen.has(message)) {
375
- continue;
376
- }
377
- seen.add(message);
378
- lines.push(message);
379
- }
380
- return lines.length > 0 ? lines.join('\n') : null;
381
- }
382
- /**
383
- * First message per field for form control binding.
384
- * @param errors - Normalized field errors.
385
- */
386
- function fieldErrorsMap(errors) {
387
- const map = {};
388
- if (!errors?.length) {
389
- return map;
390
- }
391
- for (const detail of errors) {
392
- const field = detail.field?.trim();
393
- const message = detail.message.trim();
394
- if (!field || !message || field in map) {
395
- continue;
396
- }
397
- map[field] = message;
398
- }
399
- return map;
400
- }
401
-
402
- /**
403
- * Whether the payload looks like an AFRICANIES API envelope.
404
- * @param value
405
- */
406
- function isWrappedEnvelope(value) {
407
- const record = asRecord(value);
408
- return record !== null && 'success' in record && 'data' in record;
409
- }
410
- /**
411
- * Laravel LengthAwarePaginator nested inside the envelope `data` field.
412
- * @param value
413
- */
414
- function isLaravelPaginator(value) {
415
- const record = asRecord(value);
416
- if (record === null) {
417
- return false;
418
- }
419
- return ((Array.isArray(record['data']) || record['data'] == null) &&
420
- ('current_page' in record ||
421
- 'currentPage' in record ||
422
- 'last_page' in record ||
423
- 'lastPage' in record ||
424
- 'per_page' in record ||
425
- 'perPage' in record));
426
- }
427
- /**
428
- * Map pagination from snake_case (preferred), legacy camelCase, or Laravel keys.
429
- * @param value
430
- */
431
- function normalizePagination(value) {
432
- const record = asRecord(value);
433
- if (record === null) {
434
- return null;
435
- }
436
- const current_page = record['current_page'] ?? record['currentPage'];
437
- const per_page = record['per_page'] ?? record['perPage'];
438
- const total_items = record['total_items'] ?? record['totalItems'] ?? record['total'];
439
- const total_pages = record['total_pages'] ??
440
- record['totalPages'] ??
441
- record['last_page'] ??
442
- record['lastPage'];
443
- let has_next_page = record['has_next_page'] ?? record['hasNextPage'];
444
- let has_previous_page = record['has_previous_page'] ?? record['hasPreviousPage'];
445
- if (has_next_page == null) {
446
- const next_page_url = record['next_page_url'] ?? record['nextPageUrl'];
447
- has_next_page = next_page_url != null && next_page_url !== '';
448
- }
449
- if (has_previous_page == null) {
450
- const prev_page_url = record['prev_page_url'] ?? record['prevPageUrl'];
451
- has_previous_page = prev_page_url != null && prev_page_url !== '';
452
- }
453
- return {
454
- current_page: asNumber(current_page),
455
- per_page: asNumber(per_page),
456
- total_items: asNumber(total_items),
457
- total_pages: asNumber(total_pages),
458
- has_next_page: Boolean(has_next_page),
459
- has_previous_page: Boolean(has_previous_page),
460
- };
461
- }
462
- /**
463
- * Flatten a Laravel paginator in `data` into list payload + {@link PaginationMetaModel}.
464
- * @param raw
465
- */
466
- function unwrapLaravelPaginator(raw) {
467
- if (!isLaravelPaginator(raw)) {
468
- return {
469
- data: raw ?? null,
470
- pagination: null,
471
- };
472
- }
473
- const record = asRecord(raw);
474
- if (record === null) {
475
- return { data: null, pagination: null };
476
- }
477
- return {
478
- data: Array.isArray(record['data']) ? record['data'] : [],
479
- pagination: normalizePagination(record),
480
- };
481
- }
482
- /**
483
- * Coalesce a single error entry so missing field/code become `null`.
484
- * @param value
485
- */
486
- function normalizeErrorDetail(value) {
487
- const record = asRecord(value) ?? {};
488
- return {
489
- field: asString(record['field']) || null,
490
- message: asString(record['message']),
491
- code: asString(record['code']) || null,
492
- };
493
- }
494
- /**
495
- * Resolve field errors from an envelope `errors` value or a failure `data` bag.
496
- * @param errorsRaw - Wire `errors` (array or Laravel object map).
497
- * @param data - Envelope `data` (may hold a validation bag on failure).
498
- * @param success - Envelope success flag.
499
- */
500
- function resolveErrors(errorsRaw, data, success) {
501
- if (Array.isArray(errorsRaw)) {
502
- const details = asArray(errorsRaw).map(normalizeErrorDetail);
503
- return details.length > 0 ? details : null;
504
- }
505
- if (isLaravelValidationBag(errorsRaw)) {
506
- const details = mapLaravelValidationBag(asRecord(errorsRaw) ?? {});
507
- return details.length > 0 ? details : null;
508
- }
509
- // Soft/hard validation failures often put the bag in `data` (not `errors`).
510
- if (!success && isLaravelValidationBag(data)) {
511
- const details = mapLaravelValidationBag(asRecord(data) ?? {});
512
- return details.length > 0 ? details : null;
513
- }
514
- return null;
515
- }
516
- /**
517
- * Normalize any HTTP JSON body into a fully null-safe {@link ApiResponseModel}.
518
- *
519
- * Envelope fields use snake_case where the wire does (`status_code`,
520
- * pagination keys). Paginated list reads often embed a Laravel paginator in
521
- * `data` (`data.data` + totals) — that shape is flattened here so consumers
522
- * always see `data: T[]` and `pagination`.
523
- *
524
- * Validation failures that return a Laravel field bag in `errors` or `data`
525
- * are lifted into `errors: ApiErrorDetailModel[]`. On failure, a bag in
526
- * `data` is cleared (`data: null`) so it is not typed as success payload `T`.
527
- * Top-level `message` is enriched to the joined field messages when a bag
528
- * exists (API `message` alone is often only the first field).
529
- * @param raw
530
- */
531
- function normalize(raw) {
532
- if (isWrappedEnvelope(raw)) {
533
- const record = asRecord(raw);
534
- if (record !== null) {
535
- const successRaw = record['success'];
536
- const success = typeof successRaw === 'boolean' ? successRaw : Boolean(successRaw);
537
- const status_code = record['status_code'] ?? record['statusCode'] ?? null;
538
- let pagination = normalizePagination(record['pagination']);
539
- let data = record['data'] ?? null;
540
- if (pagination === null && isLaravelPaginator(data)) {
541
- const unwrapped = unwrapLaravelPaginator(data);
542
- data = unwrapped.data ?? null;
543
- pagination = unwrapped.pagination;
544
- }
545
- const errors = resolveErrors(record['errors'], data, success);
546
- const apiMessage = asNullableString(record['message']);
547
- // Do not leave a validation bag typed as success payload `T`.
548
- if (!success && isLaravelValidationBag(data)) {
549
- data = null;
550
- }
551
- return {
552
- success,
553
- message: joinApiErrorMessages(errors) ?? apiMessage,
554
- data,
555
- errors,
556
- pagination,
557
- status_code: status_code === null || status_code === undefined
558
- ? null
559
- : asNumber(status_code),
560
- };
561
- }
562
- }
563
- return {
564
- success: true,
565
- message: null,
566
- data: raw ?? null,
567
- errors: null,
568
- pagination: null,
569
- status_code: null,
570
- };
571
- }
572
-
573
- /**
574
- * Resolve user-facing error copy from an API envelope, raw body, or HttpClient error.
575
- *
576
- * Prefers joined field messages when present; otherwise envelope / HTTP `message`.
577
- *
578
- * Prefer relying on {@link ApiClient}: HTTP failures are already rethrown as
579
- * `Error` with this text on `.message`. Hosts mainly need this helper when
580
- * calling Angular `HttpClient` directly (e.g. toast interceptor).
581
- *
582
- * @param input - Normalized response, raw JSON body, or {@link HttpErrorResponse}.
583
- */
584
- function formatApiErrorMessage(input) {
585
- if (typeof input === 'string' && input.trim()) {
586
- return input.trim();
587
- }
588
- if (input instanceof HttpErrorResponse) {
589
- const fromBody = formatApiErrorMessage(input.error);
590
- if (fromBody !== 'Something went wrong.') {
591
- return fromBody;
592
- }
593
- if (input.status === 0) {
594
- return 'Network error. Check your connection.';
595
- }
596
- if (input.status === 401 || input.status === 403) {
597
- return 'Authentication failed. Check your access token.';
598
- }
599
- if (input.status >= 500) {
600
- return 'Server error. Try again in a moment.';
601
- }
602
- if (input.message?.trim()) {
603
- return input.message.trim();
604
- }
605
- return `Request failed (${input.status})`;
606
- }
607
- if (input && typeof input === 'object' && 'success' in input && 'data' in input) {
608
- const normalized = normalize(input);
609
- return (joinApiErrorMessages(normalized.errors) ??
610
- normalized.message?.trim() ??
611
- 'Something went wrong.');
612
- }
613
- if (input && typeof input === 'object') {
614
- const record = asRecord(input);
615
- if (record &&
616
- typeof record['message'] === 'string' &&
617
- record['message'].trim()) {
618
- return record['message'].trim();
619
- }
620
- }
621
- if (input instanceof Error && input.message.trim()) {
622
- return input.message.trim();
623
- }
624
- return 'Something went wrong.';
625
- }
626
-
627
- /**
628
- * Tiny in-memory TTL cache for {@link ApiClient} GET responses.
629
- *
630
- * Intentionally process-local and non-persistent: it exists to skip identical
631
- * GETs within a short window (e.g. rapid remounts), not to replace TanStack
632
- * Query or server cache headers.
633
- *
634
- * ## When NOT to use caching (`cacheTtlMs`)
635
- *
636
- * - Frequently changing data (dashboards, live rates, inventory) — stale reads
637
- * are worse than an extra round-trip.
638
- * - User-specific or permission-sensitive payloads — another user/session in
639
- * the same SPA tab could theoretically share the in-memory map.
640
- * - After mutations that invalidate list/detail views — this cache has no
641
- * tag-based invalidation; prefer TanStack Query for that.
642
- * - Large payloads — every entry stays in heap until TTL expiry.
643
- *
644
- * Prefer omitting `cacheTtlMs` unless you have a measured hot-spot and a
645
- * known staleness budget.
646
- */
647
- /**
648
- * Key/value store with per-entry TTL eviction on read.
649
- */
650
- class HttpResponseCache {
651
- store = new Map();
652
- /**
653
- * Returns a cached value when present and not expired.
654
- *
655
- * @param key - Stable request identity (method + URL + relevant params).
656
- * @returns The stored value, or `null` on miss / expiry.
657
- */
658
- get(key) {
659
- const entry = this.store.get(key);
660
- if (!entry) {
661
- return null;
662
- }
663
- if (Date.now() >= entry.expiresAt) {
664
- this.store.delete(key);
665
- return null;
666
- }
667
- return entry.value;
668
- }
669
- /**
670
- * Stores a value until `ttlMs` elapses.
671
- *
672
- * @param key - Stable request identity.
673
- * @param value - Value to reuse for subsequent hits.
674
- * @param ttlMs - Time-to-live in milliseconds from now.
675
- */
676
- set(key, value, ttlMs) {
677
- this.store.set(key, {
678
- value,
679
- expiresAt: Date.now() + ttlMs,
680
- });
681
- }
682
- /**
683
- * Drops every entry. Useful after logout or global invalidation hooks.
684
- */
685
- clear() {
686
- this.store.clear();
687
- }
688
- }
689
-
690
- /**
691
- * Per-request shipping-mode override for {@link shipmentModeInterceptor}.
692
- *
693
- * When set, `x-shipment-mode` uses this value instead of
694
- * {@link ShippingModeService.mode}. The tab / session mode is unchanged.
695
- * Sentinel `null` means “use the active tab mode”.
696
- */
697
- const SHIPPING_MODE_OVERRIDE = new HttpContextToken(() => null);
698
- /**
699
- * Narrow unknown JSON / option values to a {@link ShippingMode}.
700
- *
701
- * @param value - Candidate value.
702
- * @returns `'stn'` or `'sfn'` when valid; otherwise `undefined`.
703
- */
704
- function asShippingMode$1(value) {
705
- return value === 'stn' || value === 'sfn' ? value : undefined;
706
- }
707
- /**
708
- * Tag a request so {@link shipmentModeInterceptor} sends a different
709
- * `x-shipment-mode` without calling {@link ShippingModeService.setMode}.
710
- *
711
- * Prefer {@link ApiRequestOptions.shippingMode} on {@link ApiClient} calls.
712
- * Use this helper with raw `HttpClient`.
713
- *
714
- * @param mode - Mode for this request only.
715
- * @param context - Existing context to merge (e.g. from {@link withToast}).
716
- * @returns HttpContext with the override set.
717
- *
718
- * @example
719
- * ```ts
720
- * this.http.post(url, body, { context: withShippingMode('stn') });
721
- * ```
722
- */
723
- function withShippingMode(mode, context) {
724
- return (context ?? new HttpContext()).set(SHIPPING_MODE_OVERRIDE, mode);
725
- }
726
-
727
- /** DI token for the HTTP → toast bridge. */
728
- const AFRICANIES_HTTP_TOAST = new InjectionToken('AFRICANIES_HTTP_TOAST');
729
- /**
730
- * Present only when the request was tagged with {@link withToast}.
731
- * Sentinel `null` means “not tagged”.
732
- */
733
- const TOAST_HTTP_OPTIONS = new HttpContextToken(() => null);
734
- /**
735
- * Opt a request into HTTP toasts.
736
- *
737
- * Defaults: `success: true`, `error: true`. Pass flags to silence either side.
738
- *
739
- * @param options - Partial overrides for the defaults.
740
- * @returns HttpContext ready to pass as `context` on HttpClient calls.
741
- *
742
- * @example
743
- * ```ts
744
- * // Success + error toasts
745
- * this.http.post(url, body, { context: withToast() });
746
- *
747
- * // Errors only
748
- * this.http.post(url, body, { context: withToast({ success: false }) });
749
- *
750
- * // Custom copy
751
- * this.http.post(url, body, {
752
- * context: withToast({
753
- * successMessage: 'Shipment saved',
754
- * errorMessage: 'Could not save shipment',
755
- * }),
756
- * });
757
- * ```
758
- */
759
- function withToast(options) {
760
- return new HttpContext().set(TOAST_HTTP_OPTIONS, {
761
- success: options?.success ?? true,
762
- error: options?.error ?? true,
763
- successMessage: options?.successMessage,
764
- errorMessage: options?.errorMessage,
765
- });
766
- }
767
-
768
- /**
769
- * Resolve {@link HttpContext} toast flags for an {@link ApiClient} request.
770
- *
771
- * Precedence:
772
- * 1. Per-request `toast: false` — never tag.
773
- * 2. GET — silent by default (list/detail failures stay in-page). Opt in with
774
- * an explicit per-request toast object.
775
- * 3. Otherwise merge config {@link AfricaniesSdkHttpToasts} with per-request overrides
776
- * (POST / PUT / PATCH / DELETE toast errors when config is `'errors'`).
777
- *
778
- * @param configMode - From {@link AfricaniesSdkConfig.httpToasts} (defaults to `'off'`).
779
- * @param perRequest - From {@link ApiRequestOptions.toast}.
780
- * @param method - HTTP verb so GET stays quiet under `'errors'` / `'all'`.
781
- */
782
- function resolveHttpToastContext(configMode, perRequest, method) {
783
- if (perRequest === false) {
784
- return undefined;
785
- }
786
- const isGet = (method ?? '').toUpperCase() === 'GET';
787
- if (isGet && perRequest == null) {
788
- return undefined;
789
- }
790
- const fromConfig = isGet ? null : configToastDefaults(configMode);
791
- const merged = perRequest
792
- ? { ...(fromConfig ?? {}), ...perRequest }
793
- : fromConfig;
794
- if (!merged) {
795
- return undefined;
796
- }
797
- const success = merged.success ?? true;
798
- const error = merged.error ?? true;
799
- if (!success && !error) {
800
- return undefined;
801
- }
802
- return withToast({
803
- success,
804
- error,
805
- successMessage: merged.successMessage,
806
- errorMessage: merged.errorMessage,
807
- });
808
- }
809
- function configToastDefaults(mode) {
810
- if (mode == null || mode === 'off') {
811
- return null;
812
- }
813
- if (mode === 'errors') {
814
- return { success: false, error: true };
815
- }
816
- if (mode === 'all') {
817
- return { success: true, error: true };
818
- }
819
- return mode;
820
- }
821
-
822
- /**
823
- * Build {@link HttpContext} for an {@link ApiClient} request.
824
- *
825
- * Merges SDK config toasts, per-request toast flags, and optional
826
- * {@link ApiRequestOptions.shippingMode} override.
827
- *
828
- * @param configMode - From {@link AfricaniesSdkConfig.httpToasts}.
829
- * @param options - Per-request {@link ApiClient} options.
830
- * @param method
831
- */
832
- function resolveApiRequestContext(configMode, options, method) {
833
- let context = resolveHttpToastContext(configMode, options.toast, method);
834
- const shippingMode = asShippingMode$1(options.shippingMode);
835
- if (shippingMode) {
836
- context = withShippingMode(shippingMode, context);
837
- }
838
- return context;
839
- }
840
- /**
841
- * Effective shipping mode for one {@link ApiClient} request.
842
- *
843
- * @param options - Per-request options (may include {@link ApiRequestOptions.shippingMode}).
844
- * @param activeMode - Current tab mode from {@link ShippingModeService}.
845
- */
846
- function resolveRequestShippingMode(options, activeMode) {
847
- return asShippingMode$1(options?.shippingMode) ?? activeMode;
848
- }
849
-
850
- /**
851
- * Build a path for the AFRICANIES list/detail `ResourceId` convention.
852
- *
853
- * | `id` | Path |
854
- * |------|------|
855
- * | `null` | `{basePath}` — paginated |
856
- * | `'all'` | `{basePath}/all` — full list |
857
- * | `number` | `{basePath}/{id}` — single record |
858
- *
859
- * @param basePath - Endpoint base (e.g. `/product/read`).
860
- * @param id - {@link ResourceId}; defaults to `null` (paginated).
861
- * @returns Absolute-or-relative path segment for {@link ApiClient.get}.
862
- *
863
- * @example
864
- * ```ts
865
- * buildResourcePath('/product/read') // '/product/read'
866
- * buildResourcePath('/product/read', 'all') // '/product/read/all'
867
- * buildResourcePath('/product/read', 42) // '/product/read/42'
868
- * ```
869
- */
870
- function buildResourcePath(basePath, id = null) {
871
- const trimmed = basePath.replace(/\/+$/, '');
872
- return id === null ? trimmed : `${trimmed}/${id}`;
873
- }
874
- /**
875
- * Build query params for a {@link ResourceId} request.
876
- *
877
- * Pagination fields (`page`, `size`, `order`) are included only when
878
- * `id === null`. Other keys are always passed through.
879
- *
880
- * Paginated lists (`id === null`) always send `size`, defaulting to
881
- * {@link DEFAULT_PAGE_SIZE} (`15`) when omitted.
882
- *
883
- * @param id - Active resource id mode.
884
- * @param params - Optional pagination + filter bag.
885
- * @returns Params object for {@link ApiClient}, or `undefined` when empty.
886
- */
887
- function buildResourceQueryParams(id, params) {
888
- const { page, size, order, ...rest } = params ?? {};
889
- const out = {};
890
- for (const [key, value] of Object.entries(rest)) {
891
- if (value === null || value === undefined || value === '') {
892
- continue;
893
- }
894
- out[key] = value;
895
- }
896
- if (id === null) {
897
- if (page !== undefined) {
898
- out['page'] = page;
899
- }
900
- out['size'] = size ?? DEFAULT_PAGE_SIZE;
901
- if (order !== undefined) {
902
- out['order'] = order;
903
- }
904
- }
905
- return Object.keys(out).length > 0 ? out : undefined;
906
- }
907
- /**
908
- * In-memory GET cache TTL for reference reads.
909
- *
910
- * Paginated lists (`id === null`) are never cached — page contents change.
911
- * `'all'` and by-id may use a short TTL for stable catalogs.
912
- *
913
- * @param id - Active resource id mode.
914
- * @param ttlMs - Desired TTL for stable reads.
915
- * @returns TTL to pass to {@link ApiClient}, or `undefined` to skip cache.
916
- */
917
- function resourceCacheTtlMs(id, ttlMs) {
918
- return id === null ? undefined : ttlMs;
919
- }
920
- /**
921
- * Map wire `data` according to {@link ResourceId} shape.
922
- *
923
- * - `null` / `'all'` → list mapper → `T[]`
924
- * - `number` → one mapper → `T` (first element if the wire sent an array)
925
- *
926
- * @typeParam T - Domain model type.
927
- * @param id - Active resource id mode.
928
- * @param raw - Envelope `data` payload.
929
- * @param mapOne - Mapper for a single record.
930
- * @param mapMany - Mapper for a list (or single object coerced to list).
931
- * @returns Mapped payload, or `null` when by-id data is missing.
932
- * List reads (`null` / `'all'`) return `[]` when `raw` is nullish.
933
- */
934
- function mapResourcePayload(id, raw, mapOne, mapMany) {
935
- if (raw == null) {
936
- return typeof id === 'number' ? null : [];
937
- }
938
- if (typeof id === 'number') {
939
- const item = Array.isArray(raw) ? raw[0] : raw;
940
- return item == null ? null : mapOne(item);
941
- }
942
- return mapMany(raw);
943
- }
944
-
945
- /**
946
- * Endpoint-agnostic HTTP façade over Angular {@link HttpClient}.
947
- *
948
- * Domain services own path strings — this client never hard-codes product
949
- * routes (except helpers like {@link getResource} that only know conventions).
950
- *
951
- * Features:
952
- * - `responseMode` overloads (`wrapped` | `raw`)
953
- * - {@link normalize} on every **2xx** body
954
- * - HTTP failures rethrown as `Error` whose `.message` is already
955
- * user-facing ({@link formatApiErrorMessage}) — consumers do not need to
956
- * parse `HttpErrorResponse` bodies
957
- * - **No automatic retry** — fail fast so UIs can show Select / table /
958
- * error-state Retry instead of a stuck loading spinner
959
- * - Optional per-GET TTL cache via `cacheTtlMs`
960
- *
961
- * @example
962
- * ```ts
963
- * const api = inject(ApiClient);
964
- * api.get<User>('/users/1').subscribe({
965
- * next: (res) => {
966
- * if (res.success) console.log(res.data);
967
- * else console.error(res.message); // already joined when a validation bag exists
968
- * },
969
- * error: (err: Error) => console.error(err.message),
970
- * });
971
- * ```
972
- */
973
- class ApiClient {
974
- http = inject(HttpClient);
975
- config = inject(AFRICANIES_SDK_CONFIG);
976
- shippingMode = inject(ShippingModeService);
977
- cache = new HttpResponseCache();
978
- get(path, options = {}) {
979
- return this.request('GET', path, undefined, options);
980
- }
981
- post(path, body, options = {}) {
982
- return this.request('POST', path, body, options);
983
- }
984
- patch(path, body, options = {}) {
985
- return this.request('PATCH', path, body, options);
986
- }
987
- put(path, body, options = {}) {
988
- return this.request('PUT', path, body, options);
989
- }
990
- delete(path, body, options = {}) {
991
- return this.request('DELETE', path, body, options);
992
- }
993
- getResource(basePath, id, query, options = {}) {
994
- const path = buildResourcePath(basePath, id);
995
- const params = {
996
- mode: resolveRequestShippingMode(options, this.shippingMode.mode()),
997
- ...(options.params ?? {}),
998
- };
999
- // Pagination applies only to the paginated-list shape (id === null).
1000
- // Size always ships — SDK default is 15 unless the caller overrides.
1001
- if (id === null) {
1002
- if (query?.page !== undefined) {
1003
- params['page'] = query.page;
1004
- }
1005
- params['size'] = query?.size ?? DEFAULT_PAGE_SIZE;
1006
- if (query?.order !== undefined) {
1007
- params['order'] = query.order;
1008
- }
1009
- }
1010
- const { responseMode: _ignored, ...requestOptions } = options;
1011
- return this.get(path, { ...requestOptions, params });
1012
- }
1013
- /**
1014
- * Paginated list — {@link ResourceId} `null`.
1015
- * IDE-friendly alias for `getResource(basePath, null, query)`.
1016
- *
1017
- * Page size defaults to {@link DEFAULT_PAGE_SIZE} (`15`) unless
1018
- * `query.size` is set. Bind `res.pagination` to `africanies-pagination`.
1019
- *
1020
- * @typeParam T - Element type of the list.
1021
- * @param basePath - Resource base path (no trailing id segment).
1022
- * @param query - Optional page/size/order.
1023
- */
1024
- getResourcePage(basePath, query) {
1025
- return this.getResource(basePath, null, query);
1026
- }
1027
- /**
1028
- * Full unpaginated dump — {@link ResourceId} `'all'`.
1029
- * IDE-friendly alias for `getResource(basePath, 'all')`.
1030
- *
1031
- * @typeParam T - Element type of the list.
1032
- * @param basePath - Resource base path.
1033
- */
1034
- getResourceAll(basePath) {
1035
- return this.getResource(basePath, 'all');
1036
- }
1037
- /**
1038
- * Single record — {@link ResourceId} number.
1039
- * IDE-friendly alias for `getResource(basePath, id)`.
1040
- *
1041
- * @typeParam T - Record type.
1042
- * @param basePath - Resource base path.
1043
- * @param id - Numeric primary key.
1044
- */
1045
- getResourceById(basePath, id) {
1046
- return this.getResource(basePath, id);
1047
- }
1048
- /**
1049
- * Clears the in-memory GET cache (e.g. on logout).
1050
- */
1051
- clearCache() {
1052
- this.cache.clear();
1053
- }
1054
- buildHttpContext(options, method) {
1055
- return resolveApiRequestContext(this.config.httpToasts, options, method);
1056
- }
1057
- request(method, path, body, options) {
1058
- const responseMode = options.responseMode ?? 'wrapped';
1059
- const url = this.resolveUrl(path);
1060
- const hasBody = method === 'POST' ||
1061
- method === 'PUT' ||
1062
- method === 'PATCH' ||
1063
- (method === 'DELETE' && body !== undefined && body !== null);
1064
- const isFormData = typeof FormData !== 'undefined' && body instanceof FormData;
1065
- // FormData must not send application/json — the browser sets multipart + boundary.
1066
- let headers = this.buildHeaders(options.headers, hasBody && !isFormData);
1067
- if (isFormData && headers.has('Content-Type')) {
1068
- headers = headers.delete('Content-Type');
1069
- }
1070
- const params = this.buildParams(options.params);
1071
- const context = this.buildHttpContext(options, method);
1072
- const cacheKey = method === 'GET' && options.cacheTtlMs != null
1073
- ? `${method} ${url}?${params.toString()}`
1074
- : null;
1075
- if (cacheKey) {
1076
- const hit = this.cache.get(cacheKey);
1077
- if (hit !== null) {
1078
- return of(this.unwrap(hit, responseMode));
1079
- }
1080
- }
1081
- let req$;
1082
- const httpOpts = context ? { headers, params, context } : { headers, params };
1083
- switch (method) {
1084
- case 'GET':
1085
- req$ = this.http.get(url, httpOpts);
1086
- break;
1087
- case 'POST':
1088
- req$ = this.http.post(url, body, httpOpts);
1089
- break;
1090
- case 'PUT':
1091
- req$ = this.http.put(url, body, httpOpts);
1092
- break;
1093
- case 'PATCH':
1094
- req$ = this.http.patch(url, body, httpOpts);
1095
- break;
1096
- case 'DELETE':
1097
- req$ = this.http.delete(url, hasBody ? { ...httpOpts, body } : httpOpts);
1098
- break;
1099
- }
1100
- // No automatic GET retry — admin shells own Retry via UI
1101
- // (`africanies-select` showRetry, table error→refreshClick, error-state).
1102
- if (this.config.timeout != null) {
1103
- req$ = req$.pipe(timeout({
1104
- each: this.config.timeout,
1105
- with: () => throwError(() => new Error(`AFRICANIES API request timed out after ${this.config.timeout}ms: ${method} ${url}`)),
1106
- }));
1107
- }
1108
- return req$.pipe(map$1((raw) => normalize(raw)), tap$1((envelope) => {
1109
- if (cacheKey && options.cacheTtlMs != null) {
1110
- this.cache.set(cacheKey, envelope, options.cacheTtlMs);
1111
- }
1112
- }), map$1((envelope) => this.unwrap(envelope, responseMode)), catchError((err) => throwError(() => new Error(formatApiErrorMessage(err)))));
1113
- }
1114
- unwrap(envelope, responseMode) {
1115
- if (responseMode === 'raw') {
1116
- // Callers opting into raw want the payload; null stays null-compatible via cast.
1117
- return envelope.data;
1118
- }
1119
- return envelope;
1120
- }
1121
- resolveUrl(path) {
1122
- if (/^https?:\/\//i.test(path)) {
1123
- return path;
1124
- }
1125
- const base = this.config.baseUrl.replace(/\/+$/, '');
1126
- const suffix = path.startsWith('/') ? path : `/${path}`;
1127
- return `${base}${suffix}`;
1128
- }
1129
- buildHeaders(extra, withJsonContentType = false) {
1130
- let headers = new HttpHeaders({
1131
- Accept: 'application/json',
1132
- ...(this.config.defaultHeaders ?? {}),
1133
- ...(extra ?? {}),
1134
- });
1135
- // Only set JSON content-type when sending a body; avoid spurious GET headers.
1136
- if (withJsonContentType && !headers.has('Content-Type')) {
1137
- headers = headers.set('Content-Type', 'application/json');
1138
- }
1139
- return headers;
1140
- }
1141
- buildParams(params) {
1142
- let httpParams = new HttpParams();
1143
- if (!params) {
1144
- return httpParams;
1145
- }
1146
- for (const [key, value] of Object.entries(params)) {
1147
- if (value === null || value === undefined || value === '') {
1148
- continue;
1149
- }
1150
- httpParams = httpParams.set(key, String(value));
1151
- }
1152
- return httpParams;
1153
- }
1154
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ApiClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1155
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ApiClient, providedIn: 'root' });
1156
- }
1157
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ApiClient, decorators: [{
1158
- type: Injectable,
1159
- args: [{ providedIn: 'root' }]
1160
- }] });
1161
-
1162
- /** Public mode-config endpoint path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
1163
- const MODE_CONFIG_PATH = '/public/mode/config';
1164
- /** Fallback region when the wire omits a required branch/key. */
1165
- const DEFAULT_REGION = {
1166
- dimension_unit: 'cm',
1167
- mass_unit: 'KG',
1168
- currency: 'NGN',
1169
- currency_symbol: '',
1170
- };
1171
- function asDimensionUnit(value) {
1172
- return value === 'inches' ? 'inches' : 'cm';
1173
- }
1174
- function asMassUnit(value) {
1175
- return value === 'LBS' ? 'LBS' : 'KG';
1176
- }
1177
- function asCurrency(value) {
1178
- return value === 'USD' ? 'USD' : 'NGN';
1179
- }
1180
- /**
1181
- * Map a region object into {@link ModeRegionConfigModel} (snake_case).
1182
- * @param raw
1183
- */
1184
- function mapRegionConfig(raw) {
1185
- const record = asRecord(raw) ?? {};
1186
- return {
1187
- dimension_unit: asDimensionUnit(record['dimension_unit'] ?? record['dimensionUnit']),
1188
- mass_unit: asMassUnit(record['mass_unit'] ?? record['massUnit']),
1189
- currency: asCurrency(record['currency']),
1190
- currency_symbol: asString(record['currency_symbol'] ?? record['currencySymbol']),
1191
- };
1192
- }
1193
- /**
1194
- * Map every region entry under a mode branch (`default`, `ng`, `us`, …).
1195
- * @param modeRaw
1196
- */
1197
- function mapModeRegions(modeRaw) {
1198
- const mode = asRecord(modeRaw) ?? {};
1199
- const out = {};
1200
- for (const [key, value] of Object.entries(mode)) {
1201
- out[key] = mapRegionConfig(value);
1202
- }
1203
- return out;
1204
- }
1205
- function regionOrDefault(regions, key) {
1206
- return regions[key] ?? regions['default'] ?? { ...DEFAULT_REGION };
1207
- }
1208
- /**
1209
- * Deep-map mode config preserving snake_case region fields.
1210
- * @param raw
1211
- */
1212
- function mapModeConfigData(raw) {
1213
- const record = asRecord(raw) ?? {};
1214
- const sfnRegions = mapModeRegions(record['sfn']);
1215
- const stnRegions = mapModeRegions(record['stn']);
1216
- const sfn = {
1217
- default: regionOrDefault(sfnRegions, 'default'),
1218
- ng: regionOrDefault(sfnRegions, 'ng'),
1219
- };
1220
- const stn = {
1221
- default: regionOrDefault(stnRegions, 'default'),
1222
- us: regionOrDefault(stnRegions, 'us'),
1223
- cn: regionOrDefault(stnRegions, 'cn'),
1224
- gb: regionOrDefault(stnRegions, 'gb'),
1225
- };
1226
- return { sfn, stn };
1227
- }
1228
- /**
1229
- * Minimal runtime guard for hydrated storage payloads.
1230
- * @param value
1231
- */
1232
- function isModeConfigData(value) {
1233
- if (value === null || typeof value !== 'object') {
1234
- return false;
1235
- }
1236
- const record = value;
1237
- const sfn = record['sfn'];
1238
- const stn = record['stn'];
1239
- if (sfn === null || typeof sfn !== 'object' || Array.isArray(sfn)) {
1240
- return false;
1241
- }
1242
- if (stn === null || typeof stn !== 'object' || Array.isArray(stn)) {
1243
- return false;
1244
- }
1245
- const sfnRecord = sfn;
1246
- const stnRecord = stn;
1247
- return (sfnRecord['default'] != null &&
1248
- typeof sfnRecord['default'] === 'object' &&
1249
- stnRecord['default'] != null &&
1250
- typeof stnRecord['default'] === 'object');
1251
- }
1252
- /**
1253
- * Resolve currency and measurement units for a country within a shipping mode.
1254
- * @param config
1255
- * @param mode
1256
- * @param countryCode
1257
- */
1258
- function resolveModeRegionConfig(config, mode, countryCode) {
1259
- const regions = mode === 'sfn' ? config?.sfn : config?.stn;
1260
- const keyed = (regions ?? {});
1261
- if (countryCode != null && countryCode !== '') {
1262
- const key = asString(countryCode).toLowerCase();
1263
- if (key !== 'default') {
1264
- const region = keyed[key];
1265
- if (region != null) {
1266
- return region;
1267
- }
1268
- }
1269
- }
1270
- return keyed['default'] ?? { ...DEFAULT_REGION };
1271
- }
1272
-
1273
- /**
1274
- * Source of truth for region currency and measurement units (STN / SFN).
1275
- *
1276
- * On startup (via {@link provideModeConfig}), loads
1277
- * `GET /public/mode/config`, normalizes snake_case wire fields once, and
1278
- * persists the mapped record to storage. {@link getRegionConfig} reads that
1279
- * saved record and resolves by country code + active shipping mode.
1280
- *
1281
- * @example
1282
- * ```ts
1283
- * // app.config.ts
1284
- * providers: [
1285
- * provideAfricaniesSdk({ baseUrl: 'https://test-api-export.africaniestest.com/api' }),
1286
- * provideHttpClient(withInterceptors([shipmentModeInterceptor])),
1287
- * provideModeConfig(),
1288
- * ];
1289
- *
1290
- * // feature — format for shipment origin country
1291
- * const modeConfig = inject(ModeConfigService);
1292
- * const region = modeConfig.getRegionConfig('us');
1293
- * if (region) {
1294
- * console.log(region.currency_symbol, region.mass_unit);
1295
- * }
1296
- * ```
1297
- */
1298
- class ModeConfigService {
1299
- api = inject(ApiClient);
1300
- storage = inject(STORAGE_TOKEN);
1301
- shippingMode = inject(ShippingModeService);
1302
- _config = signal(null, /* @ts-ignore */
1303
- ...(ngDevMode ? [{ debugName: "_config" }] : /* istanbul ignore next */ []));
1304
- _loading = signal(false, /* @ts-ignore */
1305
- ...(ngDevMode ? [{ debugName: "_loading" }] : /* istanbul ignore next */ []));
1306
- /** Latest mode-config record (storage hydrate or last successful fetch). */
1307
- config = this._config.asReadonly();
1308
- /** `true` while {@link loadConfig} is in flight. */
1309
- loading = this._loading.asReadonly();
1310
- constructor() {
1311
- this.hydrateFromStorage();
1312
- }
1313
- /**
1314
- * Fetches mode config from the server, updates {@link config}, and persists.
1315
- *
1316
- * @returns Normalized API envelope; errors propagate to subscribers.
1317
- */
1318
- loadConfig() {
1319
- this._loading.set(true);
1320
- return this.api.get(MODE_CONFIG_PATH).pipe(tap$1((res) => {
1321
- if (res.success && res.data !== null) {
1322
- this.saveRecord(mapModeConfigData(res.data));
1323
- }
1324
- }), finalize(() => {
1325
- this._loading.set(false);
1326
- }));
1327
- }
1328
- /**
1329
- * Region units and currency for a country code under the active (or given) mode.
1330
- *
1331
- * @param countryCode - e.g. `'ng'`, `'us'`, `'cn'` — unknown keys use `default`.
1332
- * @param appType - Defaults to {@link ShippingModeService.mode}.
1333
- * @returns Resolved region, or `null` before the first load/hydrate.
1334
- */
1335
- getRegionConfig(countryCode, appType) {
1336
- const config = this._config();
1337
- if (config === null) {
1338
- return null;
1339
- }
1340
- const mode = appType ?? this.shippingMode.mode();
1341
- return resolveModeRegionConfig(config, mode, countryCode);
1342
- }
1343
- /**
1344
- * Replace the in-memory record and persist — used after {@link loadConfig}.
1345
- * @param config
1346
- */
1347
- saveRecord(config) {
1348
- this._config.set(config);
1349
- this.storage.set(AFRICANIES_MODE_CONFIG_KEY, config);
1350
- }
1351
- /** Restore the last saved server record so region lookups work offline. */
1352
- hydrateFromStorage() {
1353
- const stored = this.storage.get(AFRICANIES_MODE_CONFIG_KEY);
1354
- if (isModeConfigData(stored)) {
1355
- this._config.set(stored);
1356
- }
1357
- }
1358
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ModeConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1359
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ModeConfigService, providedIn: 'root' });
1360
- }
1361
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ModeConfigService, decorators: [{
1362
- type: Injectable,
1363
- args: [{ providedIn: 'root' }]
1364
- }], ctorParameters: () => [] });
1365
-
1366
- /**
1367
- * Fetches `/public/mode/config` on app startup and hydrates {@link ModeConfigService}.
1368
- *
1369
- * Failures do not block bootstrap — cached storage (if any) remains usable.
1370
- * Pair with {@link provideAfricaniesSdk} and a real {@link AfricaniesSdkConfig.baseUrl}.
1371
- *
1372
- * @returns Environment providers that run the mode-config initializer.
1373
- */
1374
- function provideModeConfig() {
1375
- return makeEnvironmentProviders([
1376
- provideAppInitializer(() => {
1377
- const modeConfig = inject(ModeConfigService);
1378
- return firstValueFrom(modeConfig.loadConfig().pipe(catchError(() => of(null))));
1379
- }),
1380
- ]);
1381
- }
1382
-
1383
- /**
1384
- * Registers SDK configuration and ensures browser storage is available.
1385
- *
1386
- * Also calls {@link provideLocalStorage} so theme / mode-config / auth-token
1387
- * persistence works out of the box. {@link ShippingModeService} always uses
1388
- * {@link SessionStorageService} so each tab can hold its own STN/SFN mode.
1389
- * When {@link AfricaniesSdkConfig.loadModeConfig} is not `false`, {@link provideModeConfig}
1390
- * runs on startup.
1391
- *
1392
- * @param config - API origin and optional timeout / default headers.
1393
- * @returns Environment providers for `app.config.ts`.
1394
- *
1395
- * @example
1396
- * ```ts
1397
- * // app.config.ts
1398
- * import { ApplicationConfig } from '@angular/core';
1399
- * import {
1400
- * provideAfricaniesSdk,
1401
- * provideAfricaniesHttpClient,
1402
- * } from '@africanies/africanies-core';
1403
- * import { provideSessionStorage } from '@africanies/africanies-storage';
1404
- *
1405
- * export const appConfig: ApplicationConfig = {
1406
- * providers: [
1407
- * provideAfricaniesSdk({
1408
- * baseUrl: import.meta.env['NG_APP_API_URL'],
1409
- * timeout: 30_000,
1410
- * }),
1411
- * // Optional: override the localStorage default registered above
1412
- * // provideSessionStorage(),
1413
- * provideAfricaniesHttpClient(),
1414
- * // Or with app interceptors:
1415
- * // provideAfricaniesHttpClient({ interceptors: [loggingInterceptor] }),
1416
- * ],
1417
- * };
1418
- * ```
1419
- */
1420
- function provideAfricaniesSdk(config) {
1421
- return makeEnvironmentProviders([
1422
- { provide: AFRICANIES_SDK_CONFIG, useValue: config },
1423
- provideLocalStorage(),
1424
- ...(config.loadModeConfig !== false ? [provideModeConfig()] : []),
1425
- ]);
1426
- }
1427
-
1428
- /** Forgot-password path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
1429
- const AUTH_FORGOT_PASSWORD_PATH = '/auth/forgot/password';
1430
-
1431
- /**
1432
- * Unauthenticated auth endpoints (`POST /auth/…`).
1433
- *
1434
- * **Forgot password is email-only.** `forgot()` POSTs `{ email }`; the
1435
- * backend emails a reset link. This product UI never collects a new password
1436
- * for that link (no token in the route, `/onboarding/reset-password` is
1437
- * **not** that flow).
1438
- *
1439
- * `/onboarding/reset-password` is first login with a default password: after
1440
- * login, if `user.default_password` is set, the host sends the user there to
1441
- * change current → new via {@link UserService.changePassword}.
1442
- *
1443
- * Token persistence stays on {@link AuthTokenService}. These calls do not
1444
- * require a bearer token — the interceptor omits `Authorization` when none
1445
- * is stored. Admin user/partner screens reuse {@link forgot} with that
1446
- * user’s email.
1447
- *
1448
- * @example
1449
- * ```ts
1450
- * const authApi = inject(AuthService);
1451
- *
1452
- * authApi.forgot('user@example.com').subscribe((res) => {
1453
- * if (res.success) {
1454
- * // Show res.message, then “Reset completed? Login here”.
1455
- * }
1456
- * });
1457
- * ```
1458
- */
1459
- class AuthService {
1460
- api = inject(ApiClient);
1461
- /**
1462
- * Request a password-reset email (`POST /auth/forgot/password`).
1463
- *
1464
- * Enable submit only when the address looks valid. Wire `data` is an empty
1465
- * array on success — use {@link ApiResponseModel.message} for the banner.
1466
- *
1467
- * @param email - Registered account email.
1468
- * @returns Normalized envelope (`data` is typically `[]`).
1469
- */
1470
- forgot(email) {
1471
- return this.api.post(AUTH_FORGOT_PASSWORD_PATH, { email: asString(email).trim() });
1472
- }
1473
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1474
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthService, providedIn: 'root' });
1475
- }
1476
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthService, decorators: [{
1477
- type: Injectable,
1478
- args: [{ providedIn: 'root' }]
1479
- }] });
1480
-
1481
- /**
1482
- * Holds the bearer access token used by {@link authInterceptor}.
1483
- *
1484
- * After login/register in the host app, call {@link set}. The SDK persists the
1485
- * token via {@link STORAGE_TOKEN} and attaches `Authorization: Bearer …` on
1486
- * outbound HTTP. On logout, call {@link UserService.logoutFromAllSessions}
1487
- * while the token is still set, then {@link clear} (also clears the GET cache).
1488
- *
1489
- * @example
1490
- * ```ts
1491
- * const auth = inject(AuthTokenService);
1492
- * const users = inject(UserService);
1493
- *
1494
- * // after login
1495
- * auth.set(res.access_token);
1496
- *
1497
- * // later — UserService.me() is authenticated automatically
1498
- * // on logout — POST while the token is still set, then drop it
1499
- * users.logoutFromAllSessions().subscribe({
1500
- * next: () => auth.clear(),
1501
- * error: () => auth.clear(),
1502
- * });
1503
- * ```
1504
- */
1505
- class AuthTokenService {
1506
- storage = inject(STORAGE_TOKEN);
1507
- api = inject(ApiClient);
1508
- _token = signal(this.readInitialToken(), /* @ts-ignore */
1509
- ...(ngDevMode ? [{ debugName: "_token" }] : /* istanbul ignore next */ []));
1510
- /** Read-only view of the current access token. */
1511
- token = this._token.asReadonly();
1512
- /**
1513
- * Current access token, or `null` when logged out / unset.
1514
- */
1515
- get() {
1516
- return this._token();
1517
- }
1518
- /**
1519
- * Persist and activate an access token from login/register.
1520
- *
1521
- * @param accessToken - Bearer token string (whitespace-only is treated as clear).
1522
- */
1523
- set(accessToken) {
1524
- const trimmed = asString(accessToken).trim();
1525
- if (trimmed === '') {
1526
- this.clear();
1527
- return;
1528
- }
1529
- this._token.set(trimmed);
1530
- this.storage.set(AFRICANIES_ACCESS_TOKEN_KEY, trimmed);
1531
- }
1532
- /**
1533
- * Remove the token from memory and storage, and clear the HTTP GET cache.
1534
- */
1535
- clear() {
1536
- this._token.set(null);
1537
- this.storage.remove(AFRICANIES_ACCESS_TOKEN_KEY);
1538
- this.api.clearCache();
1539
- }
1540
- readInitialToken() {
1541
- const stored = this.storage.get(AFRICANIES_ACCESS_TOKEN_KEY);
1542
- if (typeof stored !== 'string') {
1543
- return null;
1544
- }
1545
- const trimmed = stored.trim();
1546
- return trimmed === '' ? null : trimmed;
1547
- }
1548
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthTokenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1549
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthTokenService, providedIn: 'root' });
1550
- }
1551
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AuthTokenService, decorators: [{
1552
- type: Injectable,
1553
- args: [{ providedIn: 'root' }]
1554
- }] });
1555
-
1556
- /**
1557
- * Enable forgot-password submit when the address looks like an email.
1558
- *
1559
- * Intentionally loose (`local@host.tld`) — the API owns uniqueness and
1560
- * deliverability. Empty / whitespace-only values fail.
1561
- *
1562
- * @param value - Raw field value.
1563
- * @returns Whether submit should be enabled.
1564
- */
1565
- function isValidEmail(value) {
1566
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(asString(value).trim());
1567
- }
1568
-
1569
- /**
1570
- * Attaches the current shipping mode as an `x-shipment-mode` header.
1571
- *
1572
- * Uses {@link SHIPPING_MODE_OVERRIDE} when the request was tagged (via
1573
- * {@link ApiRequestOptions.shippingMode} or {@link withShippingMode});
1574
- * otherwise {@link ShippingModeService.mode}. The tab mode is never mutated.
1575
- *
1576
- * Some backends also expect `mode` as a query param (see {@link ApiClient.getResource});
1577
- * this interceptor covers the header half so domain calls stay consistent
1578
- * without each service remembering the header name.
1579
- *
1580
- * Prefer {@link provideAfricaniesHttpClient}, which registers this interceptor by
1581
- * default. {@link ShippingModeService} is `providedIn: 'root'` and needs no
1582
- * extra provider.
1583
- *
1584
- * @param req - Outgoing request.
1585
- * @param next - Next handler in the interceptor chain.
1586
- * @returns The downstream observable for the cloned request.
1587
- * @example
1588
- * ```ts
1589
- * provideAfricaniesSdk({ baseUrl: 'https://api.example.com' }),
1590
- * provideAfricaniesHttpClient(),
1591
- *
1592
- * api.post('/claim', body, { shippingMode: 'stn' });
1593
- * ```
1594
- */
1595
- const shipmentModeInterceptor = (req, next) => {
1596
- const override = req.context.get(SHIPPING_MODE_OVERRIDE);
1597
- const mode = override ?? inject(ShippingModeService).mode();
1598
- return next(req.clone({
1599
- setHeaders: { 'x-shipment-mode': mode },
1600
- }));
1601
- };
1602
-
1603
- /**
1604
- * Functional interceptor that sets `Authorization: Bearer <token>` when
1605
- * {@link AuthTokenService} has an access token (from {@link AuthTokenService.set}).
1606
- *
1607
- * Compose via {@link provideAfricaniesHttpClient} (preferred) or pass explicitly to
1608
- * `provideHttpClient(withInterceptors([...]))`.
1609
- * @param req
1610
- * @param next
1611
- */
1612
- const authInterceptor = (req, next) => {
1613
- const token = inject(AuthTokenService).get();
1614
- if (token === null || token === '') {
1615
- return next(req);
1616
- }
1617
- return next(req.clone({
1618
- setHeaders: { Authorization: `Bearer ${token}` },
1619
- }));
1620
- };
1621
-
1622
- /**
1623
- * Shows success / error toasts for requests tagged with {@link withToast}.
1624
- *
1625
- * Requires {@link AFRICANIES_HTTP_TOAST} (from `provideAfricaniesToasts` in `@africanies/africanies-ui`).
1626
- * Untagged requests never toast. Missing handler → silent no-op.
1627
- * @param req - Outgoing HTTP request.
1628
- * @param next - Next interceptor handler.
1629
- * @returns Observable for the HTTP response stream.
1630
- */
1631
- const httpToastInterceptor = (req, next) => {
1632
- const options = req.context.get(TOAST_HTTP_OPTIONS);
1633
- if (!options) {
1634
- return next(req);
1635
- }
1636
- const toast = inject(AFRICANIES_HTTP_TOAST, { optional: true });
1637
- if (!toast) {
1638
- return next(req);
1639
- }
1640
- return next(req).pipe(tap$1(() => {
1641
- if (options.success) {
1642
- toast.success(options.successMessage ?? 'Done');
1643
- }
1644
- }), catchError((err) => {
1645
- if (options.error) {
1646
- toast.error(options.errorMessage ?? defaultHttpErrorMessage(err));
1647
- }
1648
- return throwError(() => err);
1649
- }));
1650
- };
1651
- /**
1652
- * @param err - Failure from HttpClient.
1653
- * @returns User-facing copy (joined field errors when the body is a validation bag).
1654
- */
1655
- function defaultHttpErrorMessage(err) {
1656
- return formatApiErrorMessage(err);
1657
- }
1658
-
1659
- /**
1660
- * Deep-map arbitrary JSON into {@link ApiJsonValue} with no `undefined`.
1661
- *
1662
- * - `undefined` / unsupported types → `null`
1663
- * - Arrays → every element mapped (never sparse/`undefined` slots)
1664
- * - Objects → every own key mapped to {@link ApiJsonValue}
1665
- *
1666
- * @param raw - Wire value (may be missing or malformed).
1667
- * @returns Null-safe JSON tree, or `null` when `raw` is nullish/unusable.
1668
- */
1669
- function mapApiJsonValue(raw) {
1670
- if (raw === null || raw === undefined) {
1671
- return null;
1672
- }
1673
- if (typeof raw === 'string' || typeof raw === 'boolean') {
1674
- return raw;
1675
- }
1676
- if (typeof raw === 'number') {
1677
- return Number.isFinite(raw) ? raw : null;
1678
- }
1679
- if (Array.isArray(raw)) {
1680
- return raw.map((entry) => mapApiJsonValue(entry) ?? null);
1681
- }
1682
- const record = asRecord(raw);
1683
- if (record === null) {
1684
- return null;
1685
- }
1686
- const out = {};
1687
- for (const [key, value] of Object.entries(record)) {
1688
- out[key] = mapApiJsonValue(value) ?? null;
1689
- }
1690
- return out;
1691
- }
1692
- /**
1693
- * Map a wire list into a null-safe {@link ApiJsonValue} array.
1694
- * Non-arrays become `[]`.
1695
- * @param raw - Candidate list.
1696
- * @returns Mapped array (never null/undefined).
1697
- */
1698
- function mapApiJsonList(raw) {
1699
- return asArray(raw).map((entry) => mapApiJsonValue(entry) ?? null);
1700
- }
1701
-
1702
- /**
1703
- * Registers `HttpClient` with AFRICANIES default interceptors baked in.
1704
- *
1705
- * Defaults (in order):
1706
- * 1. {@link shipmentModeInterceptor} — `x-shipment-mode`
1707
- * 2. {@link authInterceptor} — `Authorization` when {@link AuthTokenService} has a token
1708
- * 3. {@link httpToastInterceptor} — toasts for requests tagged with {@link withToast}
1709
- * (no-op until `provideAfricaniesToasts()` registers {@link AFRICANIES_HTTP_TOAST})
1710
- * 4. Any {@link AfricaniesHttpClientOptions.interceptors} from the host
1711
- *
1712
- * @param options - Optional extra interceptors.
1713
- * @param features - Extra `provideHttpClient` features (`withFetch`, etc.).
1714
- * @returns Environment providers for `app.config.ts`.
1715
- *
1716
- * @example
1717
- * ```ts
1718
- * provideAfricaniesSdk({ baseUrl: 'https://api.example.com' }),
1719
- * provideAfricaniesHttpClient(),
1720
- * provideAfricaniesToasts(), // from @africanies/africanies-ui — enables HTTP toasts
1721
- * ```
1722
- */
1723
- function provideAfricaniesHttpClient(options, ...features) {
1724
- const extras = options?.interceptors ?? [];
1725
- return provideHttpClient(withInterceptors([
1726
- shipmentModeInterceptor,
1727
- authInterceptor,
1728
- httpToastInterceptor,
1729
- ...extras,
1730
- ]), ...features);
1731
- }
1732
-
1733
- /**
1734
- * Pick blocking vs keep-rows fetch for a list table.
1735
- *
1736
- * Body loading only when there is no initial data or the shipping mode
1737
- * switched. Page/size keeps rows. Tab focus and Refresh keep rows.
1738
- *
1739
- * @param options
1740
- * @param options.hasData - Rows from the *current* mode are already on screen.
1741
- * @param options.reason - What triggered this fetch.
1742
- * @returns Kind to map onto `loading` / `pageLoading` / `refreshing`.
1743
- *
1744
- * @example
1745
- * ```ts
1746
- * const kind = listFetchKind({
1747
- * hasData: this.rows().length > 0,
1748
- * reason: 'mode',
1749
- * });
1750
- * this.isLoading.set(kind === 'loading');
1751
- * if (kind === 'loading') {
1752
- * this.rows.set([]);
1753
- * }
1754
- * ```
1755
- */
1756
- function listFetchKind(options) {
1757
- if (!options.hasData || options.reason === 'mode') {
1758
- return 'loading';
1759
- }
1760
- if (options.reason === 'page') {
1761
- return 'pagination';
1762
- }
1763
- return 'refreshing';
1764
- }
1765
-
1766
- /**
1767
- * Shared TanStack Query defaults for AFRICANIES consuming apps.
1768
- *
1769
- * `@tanstack/angular-query-experimental` is **not** a dependency of this SDK —
1770
- * query cache lifetime is app state. These helpers return a plain
1771
- * `defaultOptions` object you pass into your own `QueryClient`.
1772
- *
1773
- * Pin an **exact** `@tanstack/angular-query-experimental` version in the app
1774
- * (no caret/tilde): TanStack marks the Angular adapter experimental and
1775
- * breaking changes land without major bumps.
1776
- *
1777
- * Query retries default to **0** to match {@link ApiClient} (fail fast;
1778
- * screens use manual Retry). Apps can override per `QueryClient` if needed.
1779
- */
1780
- /**
1781
- * Factory for TanStack `QueryClient` `defaultOptions`.
1782
- *
1783
- * @returns Plain defaults object — no Angular providers, no TanStack imports.
1784
- *
1785
- * @example
1786
- * ```ts
1787
- * // app.config.ts — pin exact experimental version in package.json
1788
- * import {
1789
- * provideAngularQuery,
1790
- * QueryClient,
1791
- * } from '@tanstack/angular-query-experimental';
1792
- * import { createAfricaniesQueryClientDefaults } from '@africanies/africanies-core';
1793
- *
1794
- * const queryClient = new QueryClient({
1795
- * defaultOptions: createAfricaniesQueryClientDefaults(),
1796
- * });
1797
- *
1798
- * export const appConfig = {
1799
- * providers: [provideAngularQuery(queryClient)],
1800
- * };
1801
- * ```
1802
- *
1803
- * @example
1804
- * ```ts
1805
- * // Map injectQuery() signals → AsyncQueryStateModel for <africanies-async-state>
1806
- * import type { AsyncQueryStateModel } from '@africanies/africanies-models';
1807
- * import { injectQuery } from '@tanstack/angular-query-experimental';
1808
- *
1809
- * const query = injectQuery(() => ({
1810
- * queryKey: ['shipments'],
1811
- * queryFn: () => firstValueFrom(api.getResource<Shipment>('shipments', null)),
1812
- * }));
1813
- *
1814
- * const state: AsyncQueryStateModel<Shipment[] | null> = {
1815
- * data: query.data()?.data ?? undefined,
1816
- * isLoading: query.isLoading(),
1817
- * isFetching: query.isFetching(),
1818
- * isError: query.isError(),
1819
- * error: query.error()?.message ?? null,
1820
- * };
1821
- * ```
1822
- */
1823
- function createAfricaniesQueryClientDefaults() {
1824
- return {
1825
- queries: {
1826
- staleTime: 60_000,
1827
- gcTime: 5 * 60_000,
1828
- retry: 0,
1829
- retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30_000),
1830
- },
1831
- mutations: {
1832
- retry: 0,
1833
- },
1834
- };
1835
- }
1836
- /**
1837
- * Alias of {@link createAfricaniesQueryClientDefaults} for apps that prefer a
1838
- * `provide*` naming style beside {@link provideAfricaniesSdk}.
1839
- *
1840
- * Returns a plain object (not `EnvironmentProviders`) so it can be passed
1841
- * straight into `new QueryClient({ defaultOptions: ... })`.
1842
- *
1843
- * @returns Same object as {@link createAfricaniesQueryClientDefaults}.
1844
- *
1845
- * @example
1846
- * ```ts
1847
- * const queryClient = new QueryClient({
1848
- * defaultOptions: provideAfricaniesQueryDefaults(),
1849
- * });
1850
- * ```
1851
- */
1852
- function provideAfricaniesQueryDefaults() {
1853
- return createAfricaniesQueryClientDefaults();
1854
- }
1855
-
1856
- /** Public country-read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
1857
- const COUNTRY_READ_PATH = '/public/country/read';
1858
- /**
1859
- * Map a wire state object into {@link CountryStateModel} (snake_case preserved).
1860
- * @param raw - State object from the wire.
1861
- * @returns Normalized {@link CountryStateModel}.
1862
- */
1863
- function mapCountryState(raw) {
1864
- const record = asRecord(raw) ?? {};
1865
- return {
1866
- name: asString(record['name']),
1867
- state_code: asString(record['state_code'] ?? record['stateCode']),
1868
- };
1869
- }
1870
- /**
1871
- * Map a wire country object into {@link CountryModel} (snake_case preserved).
1872
- * @param raw - Country object from the wire.
1873
- * @returns Normalized {@link CountryModel}.
1874
- */
1875
- function mapCountry(raw) {
1876
- const record = asRecord(raw) ?? {};
1877
- return {
1878
- id: asNumber(record['id']),
1879
- name: asString(record['name']),
1880
- iso3: asString(record['iso3']),
1881
- iso2: asString(record['iso2']),
1882
- states: mapArray(record['states'], mapCountryState),
1883
- };
1884
- }
1885
- /**
1886
- * Map a list (or single object) payload into {@link CountryModel}[].
1887
- *
1888
- * @param raw - `data` payload from `/public/country/read/{id|all}`.
1889
- * @returns Mapped country list (empty when `raw` is null/undefined).
1890
- */
1891
- function mapCountryList(raw) {
1892
- return mapList(raw, mapCountry);
1893
- }
1894
-
1895
- /** In-memory GET cache TTL for country reference dumps / by-id (5 minutes). */
1896
- const COUNTRY_CACHE_TTL_MS = 5 * 60_000;
1897
- /**
1898
- * Public country utility reads (`GET /public/country/read/{id?}`).
1899
- *
1900
- * Uses the AFRICANIES {@link ResourceId} convention:
1901
- * - `null` (default) → paginated page
1902
- * - `'all'` → full list
1903
- * - `number` → single {@link CountryModel}
1904
- *
1905
- * @example
1906
- * ```ts
1907
- * const countries = inject(CountryService);
1908
- *
1909
- * countries.read(null, { page: 1 }).subscribe((res) => {
1910
- * console.log(res.data, res.pagination);
1911
- * });
1912
- *
1913
- * countries.readAll().subscribe((res) => console.log(res.data?.length));
1914
- * countries.readById(1).subscribe((res) => console.log(res.data?.name));
1915
- * ```
1916
- */
1917
- class CountryService {
1918
- api = inject(ApiClient);
1919
- read(id = null, params) {
1920
- return this.api
1921
- .get(buildResourcePath(COUNTRY_READ_PATH, id), {
1922
- params: buildResourceQueryParams(id, params),
1923
- cacheTtlMs: resourceCacheTtlMs(id, COUNTRY_CACHE_TTL_MS),
1924
- })
1925
- .pipe(map$1((res) => ({
1926
- ...res,
1927
- data: mapResourcePayload(id, res.data, mapCountry, mapCountryList),
1928
- })));
1929
- }
1930
- /**
1931
- * Paginated page — alias for {@link read}(`null`, params).
1932
- * @param params
1933
- */
1934
- readPage(params) {
1935
- return this.read(null, params);
1936
- }
1937
- /**
1938
- * Full list — alias for {@link read}(`'all'`).
1939
- * @param params
1940
- */
1941
- readAll(params) {
1942
- return this.read('all', params);
1943
- }
1944
- /**
1945
- * Single record — alias for {@link read}(id).
1946
- * @param id
1947
- * @param params
1948
- */
1949
- readById(id, params) {
1950
- return this.read(id, params);
1951
- }
1952
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CountryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1953
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CountryService, providedIn: 'root' });
1954
- }
1955
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CountryService, decorators: [{
1956
- type: Injectable,
1957
- args: [{ providedIn: 'root' }]
1958
- }] });
1959
-
1960
- /** Default CDN for ISO country flags ([flagcdn.com](https://flagcdn.com)). */
1961
- const COUNTRY_FLAG_CDN_BASE = 'https://flagcdn.com';
1962
- /**
1963
- * Build a flagcdn.com URL from an ISO 3166-1 alpha-2 code.
1964
- *
1965
- * Examples:
1966
- * - `countryFlagUrl('NG')` → `https://flagcdn.com/w40/ng.png`
1967
- * - `countryFlagUrl('us', { width: 80, format: 'webp' })` → `https://flagcdn.com/w80/us.webp`
1968
- *
1969
- * @param iso2 - Two-letter country code (case-insensitive).
1970
- * @param options - Width, height, or format overrides.
1971
- * @returns CDN URL, or `''` when `iso2` is not two letters.
1972
- */
1973
- function countryFlagUrl(iso2, options = {}) {
1974
- const code = iso2.trim().toLowerCase();
1975
- if (!/^[a-z]{2}$/.test(code)) {
1976
- return '';
1977
- }
1978
- const format = options.format ?? 'png';
1979
- if (options.height != null) {
1980
- return `${COUNTRY_FLAG_CDN_BASE}/h${options.height}/${code}.${format}`;
1981
- }
1982
- const width = options.width ?? 40;
1983
- return `${COUNTRY_FLAG_CDN_BASE}/w${width}/${code}.${format}`;
1984
- }
1985
- /**
1986
- * Map {@link CountryModel} rows into select options with flag CDN URLs.
1987
- *
1988
- * @param countries - Countries from {@link CountryService.readAll} / `readPage`.
1989
- * @param options - Passed through to {@link countryFlagUrl}.
1990
- */
1991
- function mapCountrySelectOptions(countries, options) {
1992
- if (!countries?.length) {
1993
- return [];
1994
- }
1995
- return countries.map((country) => ({
1996
- label: country.name,
1997
- value: country.id,
1998
- iso2: country.iso2,
1999
- prefixImageUrl: countryFlagUrl(country.iso2, options),
2000
- }));
2001
- }
2002
-
2003
- /**
2004
- * Public country utility exports.
2005
- */
2006
-
2007
- /** File read path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
2008
- const FILE_READ_PATH = '/file/read';
2009
- /** Query flag for multi-file waybill reads (`POST /file/read?multiple=yes`). */
2010
- const FILE_READ_MULTIPLE_PARAM = 'yes';
2011
- /**
2012
- * Map wire `data` into {@link FileReadModel} (snake_case preserved).
2013
- *
2014
- * `POST /file/read` returns a single object in `data` (not a list).
2015
- * If the wire unexpectedly sends a one-element array, the first entry is used.
2016
- *
2017
- * @param raw - Envelope `data` from `POST /file/read`.
2018
- * @returns Normalized {@link FileReadModel}.
2019
- */
2020
- function mapFileRead(raw) {
2021
- const entry = Array.isArray(raw) ? raw[0] : raw;
2022
- const record = asRecord(entry) ?? {};
2023
- return {
2024
- mime_type: asString(record['mime_type'] ?? record['mimeType']),
2025
- base_64: asString(record['base_64'] ?? record['base64']),
2026
- url: asString(record['url']),
2027
- };
2028
- }
2029
- /**
2030
- * Map a list (or single object) payload into {@link FileReadModel}[].
2031
- *
2032
- * Used for `POST /file/read?multiple=yes` (e-commerce waybills).
2033
- *
2034
- * @param raw - Envelope `data` from multi-file reads.
2035
- * @returns Mapped file list (empty when `raw` is null/undefined).
2036
- */
2037
- function mapFileReadList(raw) {
2038
- if (Array.isArray(raw)) {
2039
- return raw.map((entry) => mapFileRead(entry));
2040
- }
2041
- if (raw == null) {
2042
- return [];
2043
- }
2044
- return [mapFileRead(raw)];
2045
- }
2046
-
2047
- /** Public document read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
2048
- const DOCUMENT_READ_PATH = '/public/document/read';
2049
- /**
2050
- * Map nested or legacy flat preview fields into {@link FileReadModel}.
2051
- * @param record - Document wire object.
2052
- */
2053
- function mapDocumentFileRef(record) {
2054
- const nested = asRecord(record['file_ref'] ?? record['fileRef']);
2055
- if (nested !== null) {
2056
- return mapFileRead(nested);
2057
- }
2058
- const mime_type = asNullableString(record['mime_type'] ?? record['mimeType']);
2059
- const base_64 = asNullableString(record['base_64'] ?? record['base64']);
2060
- const url = asNullableString(record['url']);
2061
- if (mime_type === null && base_64 === null && url === null) {
2062
- return null;
2063
- }
2064
- return mapFileRead({
2065
- mime_type,
2066
- base_64,
2067
- url,
2068
- });
2069
- }
2070
- /**
2071
- * Map a wire document into {@link DocumentModel} (snake_case preserved).
2072
- * @param raw - Single document object from the API.
2073
- * @returns Normalized {@link DocumentModel}.
2074
- */
2075
- function mapDocument(raw) {
2076
- const record = asRecord(raw) ?? {};
2077
- return {
2078
- id: asNumber(record['id']),
2079
- name: asString(record['name']),
2080
- description: asNullableString(record['description']),
2081
- type: asNullableString(record['type']),
2082
- active: asBoolean(record['active']),
2083
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
2084
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
2085
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
2086
- file_ref: mapDocumentFileRef(record),
2087
- };
2088
- }
2089
- /**
2090
- * Map a list (or single object) payload into {@link DocumentModel}[].
2091
- * @param raw - `data` payload from `/public/document/read/{id|all}`.
2092
- * @returns Mapped document list.
2093
- */
2094
- function mapDocumentList(raw) {
2095
- return mapList(raw, mapDocument);
2096
- }
2097
-
2098
- /** In-memory GET cache TTL for document reference dumps / by-id (5 minutes). */
2099
- const DOCUMENT_CACHE_TTL_MS = 5 * 60_000;
2100
- /**
2101
- * Public document catalog reads (`GET /public/document/read/{id?}`).
2102
- *
2103
- * Uses the AFRICANIES {@link ResourceId} convention:
2104
- * - `null` (default) → paginated page
2105
- * - `'all'` → full list
2106
- * - `number` → single {@link DocumentModel} (may include preview `url` / `base_64`)
2107
- *
2108
- * @example
2109
- * ```ts
2110
- * const documents = inject(DocumentService);
2111
- *
2112
- * documents.readPage({ page: 1 }).subscribe((res) => {
2113
- * console.log(res.data, res.pagination);
2114
- * });
2115
- * documents.readById(12).subscribe((res) => {
2116
- * console.log(res.data?.url ?? res.data?.base_64);
2117
- * });
2118
- * ```
2119
- */
2120
- class DocumentService {
2121
- api = inject(ApiClient);
2122
- read(id = null, params) {
2123
- return this.api
2124
- .get(buildResourcePath(DOCUMENT_READ_PATH, id), {
2125
- params: buildResourceQueryParams(id, params),
2126
- cacheTtlMs: resourceCacheTtlMs(id, DOCUMENT_CACHE_TTL_MS),
2127
- })
2128
- .pipe(map$1((res) => ({
2129
- ...res,
2130
- data: mapResourcePayload(id, res.data, mapDocument, mapDocumentList),
2131
- })));
2132
- }
2133
- /**
2134
- * Paginated page — alias for {@link read}(`null`, params).
2135
- * @param params
2136
- */
2137
- readPage(params) {
2138
- return this.read(null, params);
2139
- }
2140
- /**
2141
- * Full list — alias for {@link read}(`'all'`).
2142
- * @param params
2143
- */
2144
- readAll(params) {
2145
- return this.read('all', params);
2146
- }
2147
- /**
2148
- * Single record — alias for {@link read}(id).
2149
- * @param id
2150
- * @param params
2151
- */
2152
- readById(id, params) {
2153
- return this.read(id, params);
2154
- }
2155
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: DocumentService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2156
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: DocumentService, providedIn: 'root' });
2157
- }
2158
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: DocumentService, decorators: [{
2159
- type: Injectable,
2160
- args: [{ providedIn: 'root' }]
2161
- }] });
2162
-
2163
- /**
2164
- * Public document catalog exports.
2165
- */
2166
-
2167
- /** Public plan read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
2168
- const PLAN_READ_PATH = '/public/plan/read';
2169
- /**
2170
- * Map a wire plan package into {@link PlanPackageModel}.
2171
- * @param raw - Package object nested under a plan.
2172
- * @returns Normalized {@link PlanPackageModel}.
2173
- */
2174
- function mapPlanPackage(raw) {
2175
- const record = asRecord(raw) ?? {};
2176
- return {
2177
- id: asNumber(record['id']),
2178
- plan_id: asNullableNumber(record['plan_id'] ?? record['planId']),
2179
- company_service_id: asNullableNumber(record['company_service_id'] ?? record['companyServiceId']),
2180
- name: asString(record['name']),
2181
- metrics: asNullableString(record['metrics']),
2182
- volume: asNullableNumber(record['volume']),
2183
- discount: asNullableString(record['discount']),
2184
- model: asNullableString(record['model']),
2185
- monthly: asNullableString(record['monthly']),
2186
- quarterly: asNullableString(record['quarterly']),
2187
- biannually: asNullableString(record['biannually']),
2188
- annually: asNullableString(record['annually']),
2189
- active: asBoolean(record['active']),
2190
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
2191
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
2192
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
2193
- };
2194
- }
2195
- /**
2196
- * Map a wire plan into {@link PlanModel} (snake_case preserved).
2197
- * @param raw - Single plan object from the API.
2198
- * @returns Normalized {@link PlanModel}.
2199
- */
2200
- function mapPlan(raw) {
2201
- const record = asRecord(raw) ?? {};
2202
- return {
2203
- id: asNumber(record['id']),
2204
- name: asString(record['name']),
2205
- active: asBoolean(record['active']),
2206
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
2207
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
2208
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
2209
- packages: mapArray(record['packages'], mapPlanPackage),
2210
- };
2211
- }
2212
- /**
2213
- * Map a list (or single object) payload into {@link PlanModel}[].
2214
- * @param raw - `data` payload from `/public/plan/read/{id|all}`.
2215
- * @returns Mapped plan list.
2216
- */
2217
- function mapPlanList(raw) {
2218
- return mapList(raw, mapPlan);
2219
- }
2220
-
2221
- /** In-memory GET cache TTL for plan reference dumps / by-id (5 minutes). */
2222
- const PLAN_CACHE_TTL_MS = 5 * 60_000;
2223
- /**
2224
- * Public subscription-plan reads (`GET /public/plan/read/{id?}`).
2225
- *
2226
- * Uses the AFRICANIES {@link ResourceId} convention:
2227
- * - `null` (default) → paginated page
2228
- * - `'all'` → full list
2229
- * - `number` → single {@link PlanModel}
2230
- *
2231
- * @example
2232
- * ```ts
2233
- * const plans = inject(PlanService);
2234
- *
2235
- * plans.readPage({ page: 1, order: 'desc' }).subscribe((res) => {
2236
- * console.log(res.data, res.pagination);
2237
- * });
2238
- * plans.readById(1).subscribe((res) => {
2239
- * console.log(res.data?.packages?.length);
2240
- * });
2241
- * ```
2242
- */
2243
- class PlanService {
2244
- api = inject(ApiClient);
2245
- read(id = null, params) {
2246
- return this.api
2247
- .get(buildResourcePath(PLAN_READ_PATH, id), {
2248
- params: buildResourceQueryParams(id, params),
2249
- cacheTtlMs: resourceCacheTtlMs(id, PLAN_CACHE_TTL_MS),
2250
- })
2251
- .pipe(map$1((res) => ({
2252
- ...res,
2253
- data: mapResourcePayload(id, res.data, mapPlan, mapPlanList),
2254
- })));
2255
- }
2256
- /**
2257
- * Paginated page — alias for {@link read}(`null`, params).
2258
- * @param params
2259
- */
2260
- readPage(params) {
2261
- return this.read(null, params);
2262
- }
2263
- /**
2264
- * Full list — alias for {@link read}(`'all'`).
2265
- * @param params
2266
- */
2267
- readAll(params) {
2268
- return this.read('all', params);
2269
- }
2270
- /**
2271
- * Single record — alias for {@link read}(id).
2272
- * @param id
2273
- * @param params
2274
- */
2275
- readById(id, params) {
2276
- return this.read(id, params);
2277
- }
2278
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PlanService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2279
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PlanService, providedIn: 'root' });
2280
- }
2281
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PlanService, decorators: [{
2282
- type: Injectable,
2283
- args: [{ providedIn: 'root' }]
2284
- }] });
2285
-
2286
- /**
2287
- * Public plan catalog exports.
2288
- */
2289
-
2290
- /** Public service read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
2291
- const SERVICE_READ_PATH = '/public/service/read';
2292
- /**
2293
- * Map a wire service into {@link ServiceModel} (snake_case preserved).
2294
- * @param raw - Single service object from the API.
2295
- * @returns Normalized {@link ServiceModel}.
2296
- */
2297
- function mapService(raw) {
2298
- const record = asRecord(raw) ?? {};
2299
- return {
2300
- id: asNumber(record['id']),
2301
- name: asString(record['name']),
2302
- description: asNullableString(record['description']),
2303
- model: asString(record['model']),
2304
- active: asBoolean(record['active']),
2305
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
2306
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
2307
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
2308
- };
2309
- }
2310
- /**
2311
- * Map a list (or single object) payload into {@link ServiceModel}[].
2312
- * @param raw - `data` payload from `/public/service/read/{id|all}`.
2313
- * @returns Mapped service list.
2314
- */
2315
- function mapServiceList(raw) {
2316
- return mapList(raw, mapService);
2317
- }
2318
-
2319
- /** In-memory GET cache TTL for service reference dumps / by-id (5 minutes). */
2320
- const SERVICE_CACHE_TTL_MS = 5 * 60_000;
2321
- /**
2322
- * Public subscription-service reads (`GET /public/service/read/{id?}`).
2323
- *
2324
- * Uses the AFRICANIES {@link ResourceId} convention:
2325
- * - `null` (default) → paginated page
2326
- * - `'all'` → full list
2327
- * - `number` → single {@link ServiceModel}
2328
- *
2329
- * @example
2330
- * ```ts
2331
- * const services = inject(ServiceService);
2332
- *
2333
- * services.readPage({ page: 1, search: 'box' }).subscribe((res) => {
2334
- * console.log(res.data, res.pagination);
2335
- * });
2336
- * services.readAll().subscribe((res) => console.log(res.data?.length));
2337
- * services.readById(3).subscribe((res) => console.log(res.data?.name));
2338
- * ```
2339
- */
2340
- class ServiceService {
2341
- api = inject(ApiClient);
2342
- read(id = null, params) {
2343
- return this.api
2344
- .get(buildResourcePath(SERVICE_READ_PATH, id), {
2345
- params: buildResourceQueryParams(id, params),
2346
- cacheTtlMs: resourceCacheTtlMs(id, SERVICE_CACHE_TTL_MS),
2347
- })
2348
- .pipe(map$1((res) => ({
2349
- ...res,
2350
- data: mapResourcePayload(id, res.data, mapService, mapServiceList),
2351
- })));
2352
- }
2353
- /**
2354
- * Paginated page — alias for {@link read}(`null`, params).
2355
- * @param params
2356
- */
2357
- readPage(params) {
2358
- return this.read(null, params);
2359
- }
2360
- /**
2361
- * Full list — alias for {@link read}(`'all'`).
2362
- * @param params
2363
- */
2364
- readAll(params) {
2365
- return this.read('all', params);
2366
- }
2367
- /**
2368
- * Single record — alias for {@link read}(id).
2369
- * @param id
2370
- * @param params
2371
- */
2372
- readById(id, params) {
2373
- return this.read(id, params);
2374
- }
2375
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ServiceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2376
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ServiceService, providedIn: 'root' });
2377
- }
2378
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ServiceService, decorators: [{
2379
- type: Injectable,
2380
- args: [{ providedIn: 'root' }]
2381
- }] });
2382
-
2383
- /**
2384
- * Public service catalog exports.
2385
- */
2386
-
2387
- /** Currency read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
2388
- const CURRENCY_READ_PATH = '/currency/read';
2389
- /** Create path (App Settings → Currencies). */
2390
- const CURRENCY_CREATE_PATH = '/currency/create';
2391
- /** Update path — name / short_code are not sent. */
2392
- const CURRENCY_UPDATE_PATH = '/currency/update';
2393
- /** Delete path — JSON body `{ id }`. */
2394
- const CURRENCY_DELETE_PATH = '/currency/delete';
2395
- /**
2396
- * Serialize a boolean / `"1"` / `"0"` flag for currency create/update.
2397
- * @param value - Host boolean or wire flag.
2398
- * @returns `"1"` or `"0"`.
2399
- */
2400
- function toCurrencyFlag01(value) {
2401
- return toFlag01(value);
2402
- }
2403
- /**
2404
- * Map a currency ↔ payment-method pivot (snake_case preserved).
2405
- * @param raw - Pivot object from the wire.
2406
- * @returns Normalized {@link CurrencyPaymentMethodPivotModel}.
2407
- */
2408
- function mapCurrencyPaymentMethodPivot(raw) {
2409
- const record = asRecord(raw) ?? {};
2410
- return {
2411
- currency_id: asNumber(record['currency_id'] ?? record['currencyId']),
2412
- payment_method_id: asNumber(record['payment_method_id'] ?? record['paymentMethodId']),
2413
- };
2414
- }
2415
- /**
2416
- * Map a wire payment method into {@link CurrencyPaymentMethodModel}.
2417
- * @param raw - Payment-method object from the wire.
2418
- * @returns Normalized {@link CurrencyPaymentMethodModel}.
2419
- */
2420
- function mapCurrencyPaymentMethod(raw) {
2421
- const record = asRecord(raw) ?? {};
2422
- return {
2423
- id: asNumber(record['id']),
2424
- name: asString(record['name']),
2425
- model: asString(record['model']),
2426
- active: asBoolean(record['active']),
2427
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
2428
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
2429
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
2430
- pivot: mapCurrencyPaymentMethodPivot(record['pivot']),
2431
- };
2432
- }
2433
- /**
2434
- * Map a wire currency into {@link CurrencyModel} (snake_case preserved).
2435
- * @param raw - Currency object from the API.
2436
- * @returns Normalized {@link CurrencyModel}.
2437
- */
2438
- function mapCurrency(raw) {
2439
- const record = asRecord(raw) ?? {};
2440
- const payment_methods = mapArray(record['payment_methods'] ?? record['paymentMethods'], mapCurrencyPaymentMethod);
2441
- const nairaGreater = asBoolean(record['is_naira_greater'] ??
2442
- record['isNairaGreater'] ??
2443
- record['is_local_currency_greater'] ??
2444
- record['isLocalCurrencyGreater']);
2445
- return {
2446
- id: asNumber(record['id']),
2447
- name: asString(record['name']),
2448
- short_code: asString(record['short_code'] ?? record['shortCode']),
2449
- division_rate: asString(record['division_rate'] ?? record['divisionRate']),
2450
- multiplication_rate: asString(record['multiplication_rate'] ?? record['multiplicationRate']),
2451
- is_local_currency_greater: nairaGreater,
2452
- is_naira_greater: nairaGreater,
2453
- active: asBoolean(record['active']),
2454
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
2455
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
2456
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
2457
- payment_methods,
2458
- };
2459
- }
2460
- /**
2461
- * Map a list (or single object) payload into {@link CurrencyModel}[].
2462
- *
2463
- * @param raw - `data` payload from `/currency/read/{id|all}`.
2464
- * @returns Mapped currency list (empty when `raw` is null/undefined).
2465
- */
2466
- function mapCurrencyList(raw) {
2467
- return mapList(raw, mapCurrency);
2468
- }
2469
- function paymentMethodIds(raw) {
2470
- return mapArray(raw, (entry) => asNumber(entry));
2471
- }
2472
- /**
2473
- * Wire body for `POST /currency/create`.
2474
- * @param body - Host create payload.
2475
- * @returns JSON body with `"1"` / `"0"` flags.
2476
- */
2477
- function toCurrencyCreateBody(body) {
2478
- return {
2479
- name: asString(body?.name).trim(),
2480
- short_code: asString(body?.short_code).trim(),
2481
- multiplication_rate: asString(body?.multiplication_rate),
2482
- division_rate: asString(body?.division_rate),
2483
- active: toCurrencyFlag01(body?.active),
2484
- is_naira_greater: toCurrencyFlag01(body?.is_naira_greater),
2485
- payment_method_ids: paymentMethodIds(body?.payment_method_ids),
2486
- };
2487
- }
2488
- /**
2489
- * Wire body for `PUT /currency/update` (no name / short_code).
2490
- * @param body - Host update payload.
2491
- * @returns JSON body with `"1"` / `"0"` flags.
2492
- */
2493
- function toCurrencyUpdateBody(body) {
2494
- return {
2495
- id: asNumber(body?.id),
2496
- multiplication_rate: asString(body?.multiplication_rate),
2497
- division_rate: asString(body?.division_rate),
2498
- active: toCurrencyFlag01(body?.active),
2499
- is_naira_greater: toCurrencyFlag01(body?.is_naira_greater),
2500
- payment_method_ids: paymentMethodIds(body?.payment_method_ids),
2501
- };
2502
- }
2503
- /**
2504
- * Wire body for `DELETE /currency/delete`.
2505
- * @param body - Currency id wrapper.
2506
- * @returns `{ id }`.
2507
- */
2508
- function toCurrencyDeleteBody(body) {
2509
- if (typeof body === 'number') {
2510
- return { id: asNumber(body) };
2511
- }
2512
- return { id: asNumber(body?.id) };
2513
- }
2514
-
2515
- /** In-memory GET cache TTL for currency reference dumps / by-id (5 minutes). */
2516
- const CURRENCY_CACHE_TTL_MS = 5 * 60_000;
2517
- /**
2518
- * Currency App Settings API (`GET /currency/read/{id?}` plus create / update / delete).
2519
- *
2520
- * Uses the AFRICANIES {@link ResourceId} convention for reads:
2521
- * - `null` (default) → paginated page
2522
- * - `'all'` → full list
2523
- * - `number` → single {@link CurrencyModel}
2524
- *
2525
- * Paginated responses embed a Laravel paginator in `data`; {@link ApiClient}
2526
- * flattens that to `data: CurrencyModel[]` plus `pagination`.
2527
- *
2528
- * Writes send `"1"` / `"0"` flags. Delete uses a JSON `{ id }` body (not query
2529
- * params). After each write, show `res.message` and call {@link readPage} again.
2530
- *
2531
- * @example
2532
- * ```ts
2533
- * const currencies = inject(CurrencyService);
2534
- *
2535
- * currencies.readPage({ page: 1, order: 'desc' }).subscribe((res) => {
2536
- * console.log(res.data, res.pagination);
2537
- * });
2538
- * currencies.readAll().subscribe((res) => console.log(res.data?.[0]?.short_code));
2539
- * currencies.readById(5).subscribe((res) => console.log(res.data?.name));
2540
- * ```
2541
- */
2542
- class CurrencyService {
2543
- api = inject(ApiClient);
2544
- read(id = null, params) {
2545
- return this.api
2546
- .get(buildResourcePath(CURRENCY_READ_PATH, id), {
2547
- toast: false,
2548
- params: buildResourceQueryParams(id, params),
2549
- cacheTtlMs: resourceCacheTtlMs(id, CURRENCY_CACHE_TTL_MS),
2550
- })
2551
- .pipe(map$1((res) => ({
2552
- ...res,
2553
- data: mapResourcePayload(id, res.data, mapCurrency, mapCurrencyList),
2554
- })));
2555
- }
2556
- /**
2557
- * Paginated page — alias for {@link read}(`null`, params).
2558
- * @param params
2559
- */
2560
- readPage(params) {
2561
- return this.read(null, params);
2562
- }
2563
- /**
2564
- * Full list — alias for {@link read}(`'all'`).
2565
- * @param params
2566
- */
2567
- readAll(params) {
2568
- return this.read('all', params);
2569
- }
2570
- /**
2571
- * Single record — alias for {@link read}(id).
2572
- * @param id
2573
- * @param params
2574
- */
2575
- readById(id, params) {
2576
- return this.read(id, params);
2577
- }
2578
- /**
2579
- * Create a currency (`POST /currency/create`).
2580
- *
2581
- * `name` / `short_code` come from the host’s local currency list, not GET.
2582
- * Flags may be boolean or `"1"` / `"0"`; they are serialized to `"1"` / `"0"`.
2583
- *
2584
- * @param body - Create payload.
2585
- * @returns Normalized envelope — use {@link ApiResponseModel.message}.
2586
- */
2587
- create(body) {
2588
- return this.api
2589
- .post(CURRENCY_CREATE_PATH, toCurrencyCreateBody(body))
2590
- .pipe(tap$1(() => this.api.clearCache()));
2591
- }
2592
- /**
2593
- * Update rates, flags, and payment methods (`PUT /currency/update`).
2594
- *
2595
- * Name and short code are not sent. Before edit, copy the row into the form
2596
- * (`true` / `false` may stay booleans — this method serializes flags).
2597
- *
2598
- * @param body - Update payload including `id`.
2599
- * @returns Normalized envelope — use {@link ApiResponseModel.message}.
2600
- */
2601
- update(body) {
2602
- return this.api
2603
- .put(CURRENCY_UPDATE_PATH, toCurrencyUpdateBody(body))
2604
- .pipe(tap$1(() => this.api.clearCache()));
2605
- }
2606
- /**
2607
- * Delete a currency (`DELETE /currency/delete`) with JSON body `{ id }`.
2608
- *
2609
- * @param body - Currency id or `{ id }`.
2610
- * @returns Normalized envelope — use {@link ApiResponseModel.message}.
2611
- */
2612
- remove(body) {
2613
- return this.api
2614
- .delete(CURRENCY_DELETE_PATH, toCurrencyDeleteBody(body))
2615
- .pipe(tap$1(() => this.api.clearCache()));
2616
- }
2617
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CurrencyService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2618
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CurrencyService, providedIn: 'root' });
2619
- }
2620
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CurrencyService, decorators: [{
2621
- type: Injectable,
2622
- args: [{ providedIn: 'root' }]
2623
- }] });
2624
-
2625
- /**
2626
- * Currency utility exports.
2627
- */
2628
-
2629
- /** Payment-method read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
2630
- const PAYMENT_METHOD_READ_PATH = '/payment_method/read';
2631
- /** Payment-method update path (`PUT`). */
2632
- const PAYMENT_METHOD_UPDATE_PATH = '/payment_method/update';
2633
- /**
2634
- * Serialize a boolean / `"1"` / `"0"` flag for payment-method update.
2635
- * @param value - Host boolean or wire flag.
2636
- * @returns `"1"` or `"0"`.
2637
- */
2638
- function toPaymentMethodFlag01(value) {
2639
- return toFlag01(value);
2640
- }
2641
- /**
2642
- * Build the wire body for `PUT /payment_method/update`.
2643
- *
2644
- * @param body - Host update payload (`active` may be boolean).
2645
- * @returns Body with `active` as `"1"` / `"0"`.
2646
- */
2647
- function toPaymentMethodUpdateBody(body) {
2648
- return {
2649
- id: asNumber(body?.id),
2650
- name: asString(body?.name),
2651
- model: asString(body?.model),
2652
- active: toPaymentMethodFlag01(body?.active),
2653
- };
2654
- }
2655
- /**
2656
- * Map a currency nested on a payment method (rates + pivot, no processors).
2657
- * @param raw - Currency object from `currencies[]`.
2658
- * @returns Normalized {@link PaymentMethodCurrencyModel}.
2659
- */
2660
- function mapPaymentMethodCurrency(raw) {
2661
- const mapped = mapCurrency(raw);
2662
- const record = asRecord(raw) ?? {};
2663
- return {
2664
- id: mapped.id,
2665
- name: mapped.name,
2666
- short_code: mapped.short_code,
2667
- division_rate: mapped.division_rate,
2668
- multiplication_rate: mapped.multiplication_rate,
2669
- is_local_currency_greater: mapped.is_local_currency_greater,
2670
- is_naira_greater: mapped.is_naira_greater,
2671
- active: mapped.active,
2672
- deleted_at: mapped.deleted_at,
2673
- created_at: mapped.created_at,
2674
- updated_at: mapped.updated_at,
2675
- pivot: mapCurrencyPaymentMethodPivot(record['pivot']),
2676
- };
2677
- }
2678
- /**
2679
- * Map a wire payment method into {@link PaymentMethodModel}.
2680
- * @param raw - Payment-method object from the API.
2681
- * @returns Normalized {@link PaymentMethodModel}.
2682
- */
2683
- function mapPaymentMethod(raw) {
2684
- const record = asRecord(raw) ?? {};
2685
- return {
2686
- id: asNumber(record['id']),
2687
- name: asString(record['name']),
2688
- model: asString(record['model']),
2689
- active: asBoolean(record['active']),
2690
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
2691
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
2692
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
2693
- currencies: mapList(record['currencies'], mapPaymentMethodCurrency),
2694
- };
2695
- }
2696
- /**
2697
- * Map a list (or single object) payload into {@link PaymentMethodModel}[].
2698
- *
2699
- * @param raw - `data` payload from `/payment_method/read/{id|all}`.
2700
- * @returns Mapped payment-method list (empty when `raw` is null/undefined).
2701
- */
2702
- function mapPaymentMethodList(raw) {
2703
- return mapList(raw, mapPaymentMethod);
2704
- }
2705
-
2706
- /** In-memory GET cache TTL for payment-method dumps / by-id (5 minutes). */
2707
- const PAYMENT_METHOD_CACHE_TTL_MS = 5 * 60_000;
2708
- /**
2709
- * Payment-method App Settings API (`GET /payment_method/read/{id?}` plus update).
2710
- *
2711
- * Uses the AFRICANIES {@link ResourceId} convention for reads:
2712
- * - `null` (default) → paginated page
2713
- * - `'all'` → full list
2714
- * - `number` → single {@link PaymentMethodModel}
2715
- *
2716
- * Paginated responses embed a Laravel paginator in `data`; {@link ApiClient}
2717
- * flattens that to `data: PaymentMethodModel[]` plus `pagination`.
2718
- *
2719
- * The only write is {@link update} (active toggle). There is no create or
2720
- * delete on this board — processors are linked from Currencies via
2721
- * `payment_method_ids`. After update, show `res.message` and patch the row’s
2722
- * local `updated_at` — do not reload the full list.
2723
- *
2724
- * @example
2725
- * ```ts
2726
- * const methods = inject(PaymentMethodService);
2727
- *
2728
- * methods.readPage({ page: 1, order: 'desc' }).subscribe((res) => {
2729
- * console.log(res.data, res.pagination);
2730
- * });
2731
- * methods.readAll().subscribe((res) => console.log(res.data?.[0]?.name));
2732
- * methods.readById(4).subscribe((res) => console.log(res.data?.currencies));
2733
- *
2734
- * methods.update({
2735
- * id: row.id,
2736
- * name: row.name,
2737
- * model: row.model,
2738
- * active: nextActive,
2739
- * }).subscribe((res) => {
2740
- * // toast res.message; patch row.updated_at locally
2741
- * });
2742
- * ```
2743
- */
2744
- class PaymentMethodService {
2745
- api = inject(ApiClient);
2746
- read(id = null, params) {
2747
- return this.api
2748
- .get(buildResourcePath(PAYMENT_METHOD_READ_PATH, id), {
2749
- toast: false,
2750
- params: buildResourceQueryParams(id, params),
2751
- cacheTtlMs: resourceCacheTtlMs(id, PAYMENT_METHOD_CACHE_TTL_MS),
2752
- })
2753
- .pipe(map$1((res) => ({
2754
- ...res,
2755
- data: mapResourcePayload(id, res.data, mapPaymentMethod, mapPaymentMethodList),
2756
- })));
2757
- }
2758
- /**
2759
- * Paginated page — alias for {@link read}(`null`, params).
2760
- * @param params
2761
- */
2762
- readPage(params) {
2763
- return this.read(null, params);
2764
- }
2765
- /**
2766
- * Full list — alias for {@link read}(`'all'`).
2767
- * @param params
2768
- */
2769
- readAll(params) {
2770
- return this.read('all', params);
2771
- }
2772
- /**
2773
- * Single record — alias for {@link read}(id).
2774
- * @param id
2775
- * @param params
2776
- */
2777
- readById(id, params) {
2778
- return this.read(id, params);
2779
- }
2780
- /**
2781
- * Toggle / update a payment method (`PUT /payment_method/update`).
2782
- *
2783
- * App Settings only edits `active` (status switch). Resend `name` and
2784
- * `model` from the current row. `active` may be boolean or `"1"` / `"0"`;
2785
- * this method serializes to `"1"` / `"0"`.
2786
- *
2787
- * On success: show {@link ApiResponseModel.message}, patch that row’s
2788
- * local `updated_at`, and do **not** call {@link readPage} again.
2789
- *
2790
- * @param body - Update payload including `id`.
2791
- * @returns Normalized envelope — use {@link ApiResponseModel.message}.
2792
- */
2793
- update(body) {
2794
- return this.api
2795
- .put(PAYMENT_METHOD_UPDATE_PATH, toPaymentMethodUpdateBody(body))
2796
- .pipe(tap$1(() => this.api.clearCache()));
2797
- }
2798
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PaymentMethodService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2799
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PaymentMethodService, providedIn: 'root' });
2800
- }
2801
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PaymentMethodService, decorators: [{
2802
- type: Injectable,
2803
- args: [{ providedIn: 'root' }]
2804
- }] });
2805
-
2806
- /**
2807
- * Payment-method utility exports.
2808
- */
2809
-
2810
- /** Shipment-method read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
2811
- const SHIPMENT_METHOD_READ_PATH = '/shipment_method/read';
2812
- function asYesNo(value) {
2813
- if (typeof value === 'boolean') {
2814
- return value;
2815
- }
2816
- return asString(value).trim().toLowerCase() === 'yes';
2817
- }
2818
- function asShippingMode(value) {
2819
- return value === 'stn' ? 'stn' : 'sfn';
2820
- }
2821
- /**
2822
- * Map a nested zone object into {@link ShipmentZoneModel}.
2823
- * @param raw
2824
- */
2825
- function mapShipmentZone(raw) {
2826
- const record = asRecord(raw);
2827
- if (record === null) {
2828
- return null;
2829
- }
2830
- return {
2831
- id: asNumber(record['id']),
2832
- name: asString(record['name']),
2833
- type: asString(record['type']),
2834
- active: asBoolean(record['active']),
2835
- };
2836
- }
2837
- /**
2838
- * Map a method↔zone link into {@link ShipmentMethodZoneLinkModel}.
2839
- * @param raw
2840
- */
2841
- function mapShipmentMethodZoneLink(raw) {
2842
- const record = asRecord(raw) ?? {};
2843
- return {
2844
- id: asNumber(record['id']),
2845
- zone_id: asNumber(record['zone_id'] ?? record['zoneId']),
2846
- shipment_method_id: asNumber(record['shipment_method_id'] ?? record['shipmentMethodId']),
2847
- active: asBoolean(record['active']),
2848
- mode: asShippingMode(record['mode']),
2849
- zone: mapShipmentZone(record['zone']),
2850
- };
2851
- }
2852
- /**
2853
- * Map a Laravel-style `zone_values` page into {@link ShipmentMethodZonePageModel}.
2854
- * @param raw
2855
- */
2856
- function mapShipmentMethodZonePage(raw) {
2857
- const record = asRecord(raw);
2858
- if (record === null) {
2859
- return {
2860
- data: [],
2861
- current_page: 0,
2862
- per_page: 0,
2863
- total: 0,
2864
- last_page: 0,
2865
- };
2866
- }
2867
- return {
2868
- data: mapArray(record['data'] ?? record['items'], mapShipmentMethodZoneLink),
2869
- current_page: asNumber(record['current_page'] ?? record['currentPage']),
2870
- per_page: asNumber(record['per_page'] ?? record['perPage']),
2871
- total: asNumber(record['total']),
2872
- last_page: asNumber(record['last_page'] ?? record['lastPage']),
2873
- };
2874
- }
2875
- /**
2876
- * Map a wire shipment method into {@link ShipmentMethodModel} (snake_case).
2877
- * @param raw
2878
- */
2879
- function mapShipmentMethod(raw) {
2880
- const record = asRecord(raw) ?? {};
2881
- return {
2882
- id: asNumber(record['id']),
2883
- name: asString(record['name']),
2884
- slug: asString(record['slug']),
2885
- model: asString(record['model']),
2886
- min_delivery_business_day: asNumber(record['min_delivery_business_day'] ?? record['minDeliveryBusinessDay']),
2887
- max_delivery_business_day: asNumber(record['max_delivery_business_day'] ?? record['maxDeliveryBusinessDay']),
2888
- notes: asString(record['notes']),
2889
- blacklisted_words: asNullableString(record['blacklisted_words'] ?? record['blacklistedWords']),
2890
- position: asNumber(record['position']),
2891
- min_weight: asNumber(record['min_weight'] ?? record['minWeight']),
2892
- max_weight: asNumber(record['max_weight'] ?? record['maxWeight']),
2893
- max_length: asNumber(record['max_length'] ?? record['maxLength']),
2894
- max_width: asNumber(record['max_width'] ?? record['maxWidth']),
2895
- max_height: asNumber(record['max_height'] ?? record['maxHeight']),
2896
- markup: asNumber(record['markup']),
2897
- surcharge: asNumber(record['surcharge']),
2898
- insurance_benchmark: asNumber(record['insurance_benchmark'] ?? record['insuranceBenchmark']),
2899
- insurance: asNumber(record['insurance']),
2900
- clearing_handling: asNumber(record['clearing_handling'] ?? record['clearingHandling']),
2901
- destination: asString(record['destination']),
2902
- sea_only: asYesNo(record['sea_only'] ?? record['seaOnly']),
2903
- currency: asString(record['currency']),
2904
- type: asString(record['type']),
2905
- active: asBoolean(record['active']),
2906
- multiple_rates: asBoolean(record['multiple_rates'] ?? record['multipleRates']),
2907
- first_shipment_discount: asNumber(record['first_shipment_discount'] ?? record['firstShipmentDiscount']),
2908
- discount_type: asString(record['discount_type'] ?? record['discountType']),
2909
- discount_active: asBoolean(record['discount_active'] ?? record['discountActive']),
2910
- mode: asShippingMode(record['mode']),
2911
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
2912
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
2913
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
2914
- markdown: asNumber(record['markdown']),
2915
- zone_values: mapShipmentMethodZonePage(record['zone_values'] ?? record['zoneValues']),
2916
- };
2917
- }
2918
- /**
2919
- * Map a list (or single object) payload into {@link ShipmentMethodModel}[].
2920
- * @param raw
2921
- */
2922
- function mapShipmentMethodList(raw) {
2923
- return mapList(raw, mapShipmentMethod);
2924
- }
2925
-
2926
- /** In-memory GET cache TTL for method reference dumps / by-id (5 minutes). */
2927
- const SHIPMENT_METHOD_CACHE_TTL_MS = 5 * 60_000;
2928
- /**
2929
- * Shipment method / carrier utility reads (`GET /shipment_method/read/{id?}`).
2930
- *
2931
- * Uses the AFRICANIES {@link ResourceId} convention:
2932
- * - `null` (default) → paginated page
2933
- * - `'all'` → full list
2934
- * - `number` → single {@link ShipmentMethodModel}
2935
- *
2936
- * @example
2937
- * ```ts
2938
- * const methods = inject(ShipmentMethodService);
2939
- *
2940
- * methods.readPage({ page: 1 }).subscribe((res) => {
2941
- * console.log(res.data, res.pagination);
2942
- * });
2943
- * methods.readAll().subscribe((res) => console.log(res.data?.[0]?.name));
2944
- * methods.readById(12).subscribe((res) => console.log(res.data?.name));
2945
- * ```
2946
- */
2947
- class ShipmentMethodService {
2948
- api = inject(ApiClient);
2949
- read(id = null, params) {
2950
- return this.api
2951
- .get(buildResourcePath(SHIPMENT_METHOD_READ_PATH, id), {
2952
- params: buildResourceQueryParams(id, params),
2953
- cacheTtlMs: resourceCacheTtlMs(id, SHIPMENT_METHOD_CACHE_TTL_MS),
2954
- })
2955
- .pipe(map$1((res) => ({
2956
- ...res,
2957
- data: mapResourcePayload(id, res.data, mapShipmentMethod, mapShipmentMethodList),
2958
- })));
2959
- }
2960
- /**
2961
- * Paginated page — alias for {@link read}(`null`, params).
2962
- * @param params
2963
- */
2964
- readPage(params) {
2965
- return this.read(null, params);
2966
- }
2967
- /**
2968
- * Full list — alias for {@link read}(`'all'`).
2969
- * @param params
2970
- */
2971
- readAll(params) {
2972
- return this.read('all', params);
2973
- }
2974
- /**
2975
- * Single record — alias for {@link read}(id).
2976
- * @param id
2977
- * @param params
2978
- */
2979
- readById(id, params) {
2980
- return this.read(id, params);
2981
- }
2982
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ShipmentMethodService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2983
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ShipmentMethodService, providedIn: 'root' });
2984
- }
2985
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ShipmentMethodService, decorators: [{
2986
- type: Injectable,
2987
- args: [{ providedIn: 'root' }]
2988
- }] });
2989
-
2990
- /**
2991
- * Shipment method / carrier utility exports.
2992
- */
2993
-
2994
- /** Warehouse read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
2995
- const WAREHOUSE_READ_PATH = '/warehouse/read';
2996
- /**
2997
- * Map warehouse state into {@link WarehouseStateModel} (snake_case).
2998
- * @param raw
2999
- */
3000
- function mapWarehouseState(raw) {
3001
- const record = asRecord(raw);
3002
- if (record === null) {
3003
- return null;
3004
- }
3005
- return {
3006
- id: asNumber(record['id']),
3007
- name: asString(record['name']),
3008
- state_code: asString(record['state_code'] ?? record['stateCode']),
3009
- country: asString(record['country']),
3010
- country_code: asString(record['country_code'] ?? record['countryCode']),
3011
- };
3012
- }
3013
- /**
3014
- * Map nested country (same shape as public country utility).
3015
- * @param raw
3016
- */
3017
- function mapWarehouseCountry(raw) {
3018
- if (raw == null) {
3019
- return null;
3020
- }
3021
- return mapCountry(raw);
3022
- }
3023
- /**
3024
- * Map a wire warehouse into {@link WarehouseModel} (snake_case preserved).
3025
- * @param raw
3026
- */
3027
- function mapWarehouse(raw) {
3028
- const record = asRecord(raw) ?? {};
3029
- return {
3030
- id: asNumber(record['id']),
3031
- partner_id: asNullableNumber(record['partner_id'] ?? record['partnerId']),
3032
- name: asString(record['name']),
3033
- phone: asString(record['phone']),
3034
- email: asString(record['email']),
3035
- country: mapWarehouseCountry(record['country']),
3036
- api_enabled: asBoolean(record['api_enabled'] ?? record['apiEnabled']),
3037
- state: mapWarehouseState(record['state']),
3038
- city: asString(record['city']),
3039
- address: asString(record['address']),
3040
- longitude: asNumber(record['longitude']),
3041
- latitude: asNumber(record['latitude']),
3042
- zip_code: asString(record['zip_code'] ?? record['zipCode']),
3043
- usage: asNumber(record['usage']),
3044
- active: asBoolean(record['active']),
3045
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
3046
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3047
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3048
- storage_charge: asNumber(record['storage_charge'] ?? record['storageCharge']),
3049
- storage_period: asNumber(record['storage_period'] ?? record['storagePeriod']),
3050
- delivery_charge: asNumber(record['delivery_charge'] ?? record['deliveryCharge']),
3051
- delivery_count: asNumber(record['delivery_count'] ?? record['deliveryCount']),
3052
- currency: asString(record['currency']),
3053
- etw_shipment_available: asBoolean(record['etw_shipment_available'] ?? record['etwShipmentAvailable']),
3054
- local: asBoolean(record['local']),
3055
- no_shippo: asBoolean(record['no_shippo'] ?? record['noShippo']),
3056
- partner: mapApiJsonValue(record['partner']),
3057
- };
3058
- }
3059
- /**
3060
- * Map a list (or single object) payload into {@link WarehouseModel}[].
3061
- * @param raw
3062
- */
3063
- function mapWarehouseList(raw) {
3064
- return mapList(raw, mapWarehouse);
3065
- }
3066
-
3067
- /** In-memory GET cache TTL for warehouse reference dumps / by-id (5 minutes). */
3068
- const WAREHOUSE_CACHE_TTL_MS = 5 * 60_000;
3069
- /**
3070
- * Warehouse utility reads (`GET /warehouse/read/{id?}`).
3071
- *
3072
- * Uses the AFRICANIES {@link ResourceId} convention:
3073
- * - `null` (default) → paginated page
3074
- * - `'all'` → full list
3075
- * - `number` → single {@link WarehouseModel}
3076
- *
3077
- * @example
3078
- * ```ts
3079
- * const warehouses = inject(WarehouseService);
3080
- *
3081
- * warehouses.readPage({ page: 1 }).subscribe((res) => {
3082
- * console.log(res.data, res.pagination);
3083
- * });
3084
- * warehouses.readAll().subscribe((res) => console.log(res.data?.length));
3085
- * warehouses.readById(37).subscribe((res) => console.log(res.data?.name));
3086
- * ```
3087
- */
3088
- class WarehouseService {
3089
- api = inject(ApiClient);
3090
- read(id = null, params) {
3091
- return this.api
3092
- .get(buildResourcePath(WAREHOUSE_READ_PATH, id), {
3093
- toast: false,
3094
- params: buildResourceQueryParams(id, params),
3095
- cacheTtlMs: resourceCacheTtlMs(id, WAREHOUSE_CACHE_TTL_MS),
3096
- })
3097
- .pipe(map$1((res) => ({
3098
- ...res,
3099
- data: mapResourcePayload(id, res.data, mapWarehouse, mapWarehouseList),
3100
- })));
3101
- }
3102
- /**
3103
- * Paginated page — alias for {@link read}(`null`, params).
3104
- * @param params
3105
- */
3106
- readPage(params) {
3107
- return this.read(null, params);
3108
- }
3109
- /**
3110
- * Full list — alias for {@link read}(`'all'`).
3111
- * @param params
3112
- */
3113
- readAll(params) {
3114
- return this.read('all', params);
3115
- }
3116
- /**
3117
- * Single record — alias for {@link read}(id).
3118
- * @param id
3119
- * @param params
3120
- */
3121
- readById(id, params) {
3122
- return this.read(id, params);
3123
- }
3124
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: WarehouseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3125
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: WarehouseService, providedIn: 'root' });
3126
- }
3127
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: WarehouseService, decorators: [{
3128
- type: Injectable,
3129
- args: [{ providedIn: 'root' }]
3130
- }] });
3131
-
3132
- /**
3133
- * Warehouse utility exports.
3134
- */
3135
-
3136
- /** Zone read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
3137
- const ZONE_READ_PATH = '/zone/read/records';
3138
- /**
3139
- * Map a wire zone into {@link ZoneModel} (snake_case preserved).
3140
- * @param raw
3141
- */
3142
- function mapZone(raw) {
3143
- const record = asRecord(raw) ?? {};
3144
- return {
3145
- id: asNumber(record['id']),
3146
- name: asString(record['name']),
3147
- type: asString(record['type']),
3148
- active: asBoolean(record['active']),
3149
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
3150
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3151
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3152
- };
3153
- }
3154
- /**
3155
- * Map a list (or single object) payload into {@link ZoneModel}[].
3156
- * @param raw
3157
- */
3158
- function mapZoneList(raw) {
3159
- return mapList(raw, mapZone);
3160
- }
3161
-
3162
- /** In-memory GET cache TTL for zone reference dumps / by-id (5 minutes). */
3163
- const ZONE_CACHE_TTL_MS = 5 * 60_000;
3164
- /**
3165
- * Zone utility reads (`GET /zone/read/records/{id?}`).
3166
- *
3167
- * Uses the AFRICANIES {@link ResourceId} convention:
3168
- * - `null` (default) → paginated page
3169
- * - `'all'` → full list
3170
- * - `number` → single {@link ZoneModel}
3171
- *
3172
- * @example
3173
- * ```ts
3174
- * const zones = inject(ZoneService);
3175
- *
3176
- * zones.readPage({ page: 1 }).subscribe((res) => {
3177
- * console.log(res.data, res.pagination);
3178
- * });
3179
- * zones.readAll().subscribe((res) => console.log(res.data?.length));
3180
- * zones.readById(1).subscribe((res) => console.log(res.data?.name));
3181
- * ```
3182
- */
3183
- class ZoneService {
3184
- api = inject(ApiClient);
3185
- read(id = null, params) {
3186
- return this.api
3187
- .get(buildResourcePath(ZONE_READ_PATH, id), {
3188
- params: buildResourceQueryParams(id, params),
3189
- cacheTtlMs: resourceCacheTtlMs(id, ZONE_CACHE_TTL_MS),
3190
- })
3191
- .pipe(map$1((res) => ({
3192
- ...res,
3193
- data: mapResourcePayload(id, res.data, mapZone, mapZoneList),
3194
- })));
3195
- }
3196
- /**
3197
- * Paginated page — alias for {@link read}(`null`, params).
3198
- * @param params
3199
- */
3200
- readPage(params) {
3201
- return this.read(null, params);
3202
- }
3203
- /**
3204
- * Full list — alias for {@link read}(`'all'`).
3205
- * @param params
3206
- */
3207
- readAll(params) {
3208
- return this.read('all', params);
3209
- }
3210
- /**
3211
- * Single record — alias for {@link read}(id).
3212
- * @param id
3213
- * @param params
3214
- */
3215
- readById(id, params) {
3216
- return this.read(id, params);
3217
- }
3218
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ZoneService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3219
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ZoneService, providedIn: 'root' });
3220
- }
3221
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ZoneService, decorators: [{
3222
- type: Injectable,
3223
- args: [{ providedIn: 'root' }]
3224
- }] });
3225
-
3226
- /**
3227
- * Zone utility exports.
3228
- */
3229
-
3230
- /** Current-user path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
3231
- const USER_PATH = '/user';
3232
- /** Change-password path after a default-password first login. */
3233
- const USER_CHANGE_PASSWORD_PATH = '/user/change/password';
3234
- /** Invalidate every session for the signed-in user. */
3235
- const USER_LOGOUT_FROM_ALL_SESSIONS_PATH = '/user/logout-from-all-sessions';
3236
- const ACCOUNT_TYPES = new Set(['business', 'individual']);
3237
- const SHIPPING_TYPES = new Set(['instant', 'consolidation']);
3238
- const PLAN_TYPES = new Set([
3239
- 'monthly',
3240
- 'quarterly',
3241
- 'biannually',
3242
- 'annually',
3243
- ]);
3244
- const USER_MODEL_TYPES = new Set([
3245
- 'App\\Models\\Customer',
3246
- 'App\\Models\\Admin',
3247
- ]);
3248
- /**
3249
- * @param value - Candidate union member.
3250
- * @param allowed - Allowed set.
3251
- * @returns Value when allowed; otherwise `null`.
3252
- */
3253
- function asEnumMember(value, allowed) {
3254
- if (typeof value !== 'string') {
3255
- return null;
3256
- }
3257
- return allowed.has(value) ? value : null;
3258
- }
3259
- /**
3260
- * Map a state row under country.states.
3261
- * @param raw - State object from the wire.
3262
- * @returns {@link UserStateModel}, or `null`.
3263
- */
3264
- function mapUserCountryState(raw) {
3265
- const record = asRecord(raw);
3266
- if (record === null) {
3267
- return null;
3268
- }
3269
- return {
3270
- name: asNullableString(record['name']),
3271
- state_code: asNullableString(record['state_code'] ?? record['stateCode']),
3272
- };
3273
- }
3274
- /**
3275
- * Map nested country on the user profile.
3276
- * @param raw - Country object from the wire.
3277
- * @returns {@link UserCountryModel}, or `null`.
3278
- */
3279
- function mapUserCountry(raw) {
3280
- const record = asRecord(raw);
3281
- if (record === null) {
3282
- return null;
3283
- }
3284
- const states = Array.isArray(record['states'])
3285
- ? mapArray(record['states'], mapUserCountryState).filter((entry) => entry !== null)
3286
- : null;
3287
- return {
3288
- id: asNullableNumber(record['id']),
3289
- name: asNullableString(record['name']),
3290
- iso3: asNullableString(record['iso3']),
3291
- iso2: asNullableString(record['iso2']),
3292
- states,
3293
- };
3294
- }
3295
- /**
3296
- * Normalize `state` when the API returns a string (or an object with `name`).
3297
- * @param raw - Wire `state` field.
3298
- * @returns String label, or `null`.
3299
- */
3300
- function mapUserStateLabel(raw) {
3301
- if (raw == null) {
3302
- return null;
3303
- }
3304
- if (typeof raw === 'string') {
3305
- return raw;
3306
- }
3307
- const record = asRecord(raw);
3308
- if (record !== null) {
3309
- return asNullableString(record['name'] ?? record['state']);
3310
- }
3311
- return null;
3312
- }
3313
- /**
3314
- * @param raw - Plan package object.
3315
- * @returns {@link UserPlanPackageModel}, or `null`.
3316
- */
3317
- function mapUserPlanPackage(raw) {
3318
- const record = asRecord(raw);
3319
- if (record === null) {
3320
- return null;
3321
- }
3322
- return {
3323
- id: asNullableNumber(record['id']),
3324
- plan_id: asNullableNumber(record['plan_id'] ?? record['planId']),
3325
- company_service_id: asNullableNumber(record['company_service_id'] ?? record['companyServiceId']),
3326
- name: asNullableString(record['name']),
3327
- metrics: asNullableString(record['metrics']),
3328
- volume: asNullableNumber(record['volume']),
3329
- discount: asNullableString(record['discount']),
3330
- model: asNullableString(record['model']),
3331
- monthly: asNullableString(record['monthly']),
3332
- quarterly: asNullableString(record['quarterly']),
3333
- biannually: asNullableString(record['biannually']),
3334
- annually: asNullableString(record['annually']),
3335
- active: asNullableBoolean(record['active']),
3336
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
3337
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3338
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3339
- };
3340
- }
3341
- /**
3342
- * @param raw - Plan object.
3343
- * @returns {@link UserPlanModel}, or `null`.
3344
- */
3345
- function mapUserPlan(raw) {
3346
- const record = asRecord(raw);
3347
- if (record === null) {
3348
- return null;
3349
- }
3350
- const packages = Array.isArray(record['packages'])
3351
- ? mapArray(record['packages'], mapUserPlanPackage).filter((entry) => entry !== null)
3352
- : null;
3353
- return {
3354
- id: asNullableNumber(record['id']),
3355
- name: asNullableString(record['name']),
3356
- active: asNullableBoolean(record['active']),
3357
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
3358
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3359
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3360
- packages,
3361
- };
3362
- }
3363
- /**
3364
- * @param raw - Gateway payload object.
3365
- * @returns {@link UserGatewayPayloadModel}, or `null`.
3366
- */
3367
- function mapUserGatewayPayload(raw) {
3368
- const record = asRecord(raw);
3369
- if (record === null) {
3370
- return null;
3371
- }
3372
- return {
3373
- authorization_url: asNullableString(record['authorization_url'] ?? record['authorizationUrl']),
3374
- access_code: asNullableString(record['access_code'] ?? record['accessCode']),
3375
- reference: asNullableString(record['reference']),
3376
- redirect_url: asNullableString(record['redirect_url'] ?? record['redirectUrl']),
3377
- };
3378
- }
3379
- /**
3380
- * @param raw - Payment payload object (already parsed).
3381
- * @returns {@link UserPaymentPayloadModel}, or `null`.
3382
- */
3383
- function mapUserPaymentPayload(raw) {
3384
- const record = asRecord(raw);
3385
- if (record === null) {
3386
- return null;
3387
- }
3388
- return {
3389
- url: asNullableString(record['url']),
3390
- redirect_url: asNullableString(record['redirect_url'] ?? record['redirectUrl']),
3391
- gateway_payload: mapUserGatewayPayload(record['gateway_payload'] ?? record['gatewayPayload']),
3392
- reference: asNullableString(record['reference']),
3393
- };
3394
- }
3395
- /**
3396
- * @param raw - Subscription object.
3397
- * @returns {@link UserSubscriptionModel}, or `null`.
3398
- */
3399
- function mapUserSubscription(raw) {
3400
- const record = asRecord(raw);
3401
- if (record === null) {
3402
- return null;
3403
- }
3404
- return {
3405
- id: asNullableNumber(record['id']),
3406
- user_id: asNullableNumber(record['user_id'] ?? record['userId']),
3407
- plan_id: asNullableNumber(record['plan_id'] ?? record['planId']),
3408
- account_id: asNullableNumber(record['account_id'] ?? record['accountId']),
3409
- reference: asNullableString(record['reference']),
3410
- process_url: asNullableString(record['process_url'] ?? record['processUrl']),
3411
- reference_salt: asNullableString(record['reference_salt'] ?? record['referenceSalt']),
3412
- amount: asNullableString(record['amount']),
3413
- currency: asNullableString(record['currency']),
3414
- payment_amount: asNullableString(record['payment_amount'] ?? record['paymentAmount']),
3415
- payment_currency: asNullableString(record['payment_currency'] ?? record['paymentCurrency']),
3416
- coupon_id: asNullableNumber(record['coupon_id'] ?? record['couponId']),
3417
- coupon_discount: asNullableString(record['coupon_discount'] ?? record['couponDiscount']),
3418
- coupon_amount: asNullableString(record['coupon_amount'] ?? record['couponAmount']),
3419
- payment_payload: asNullableString(record['payment_payload'] ?? record['paymentPayload']),
3420
- plan_type: asEnumMember(record['plan_type'] ?? record['planType'], PLAN_TYPES),
3421
- used: asNullableBoolean(record['used']),
3422
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
3423
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3424
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3425
- };
3426
- }
3427
- /**
3428
- * @param raw - Business account object.
3429
- * @returns {@link UserBusinessAccountModel}, or `null`.
3430
- */
3431
- function mapUserBusinessAccount(raw) {
3432
- const record = asRecord(raw);
3433
- if (record === null) {
3434
- return null;
3435
- }
3436
- return {
3437
- id: asNullableNumber(record['id']),
3438
- user_id: asNullableNumber(record['user_id'] ?? record['userId']),
3439
- plan_id: asNullableNumber(record['plan_id'] ?? record['planId']),
3440
- name: asNullableString(record['name']),
3441
- account_email: asNullableString(record['account_email'] ?? record['accountEmail']),
3442
- plan_type: asEnumMember(record['plan_type'] ?? record['planType'], PLAN_TYPES),
3443
- first_payment: asNullableBoolean(record['first_payment'] ?? record['firstPayment']),
3444
- is_whitelisted: asNullableBoolean(record['is_whitelisted'] ?? record['isWhitelisted']),
3445
- no_state_validation: asNullableBoolean(record['no_state_validation'] ?? record['noStateValidation']),
3446
- show_waybill: asNullableBoolean(record['show_waybill'] ?? record['showWaybill']),
3447
- notify_api_shipment: asNullableBoolean(record['notify_api_shipment'] ?? record['notifyApiShipment']),
3448
- active: asNullableBoolean(record['active']),
3449
- expires_at: asNullableString(record['expires_at'] ?? record['expiresAt']),
3450
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
3451
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3452
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3453
- type: asEnumMember(record['type'], ACCOUNT_TYPES),
3454
- days_left: asNullableNumber(record['days_left'] ?? record['daysLeft']),
3455
- plan: mapUserPlan(record['plan']),
3456
- subscription: mapUserSubscription(record['subscription']),
3457
- };
3458
- }
3459
- /**
3460
- * @param raw - Account manager object.
3461
- * @returns {@link UserAccountManagerModel}, or `null`.
3462
- */
3463
- function mapUserAccountManager(raw) {
3464
- const record = asRecord(raw);
3465
- if (record === null) {
3466
- return null;
3467
- }
3468
- return {
3469
- id: asNullableNumber(record['id']),
3470
- user_id: asNullableNumber(record['user_id'] ?? record['userId']),
3471
- manager_id: asNullableNumber(record['manager_id'] ?? record['managerId']),
3472
- account_id: asNullableNumber(record['account_id'] ?? record['accountId']),
3473
- name: asNullableString(record['name']),
3474
- email: asNullableString(record['email']),
3475
- phone: asNullableString(record['phone']),
3476
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3477
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3478
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
3479
- };
3480
- }
3481
- /**
3482
- * Map a bare wire user object into {@link UserModel} (snake_case preserved).
3483
- * @param raw - User object from `GET /user` (unwrapped).
3484
- * @returns Normalized {@link UserModel}.
3485
- */
3486
- function mapUser(raw) {
3487
- const record = asRecord(raw) ?? {};
3488
- return {
3489
- id: asNullableNumber(record['id']),
3490
- central_id: asNullableString(record['central_id'] ?? record['centralId']),
3491
- name: asNullableString(record['name']),
3492
- first_name: asNullableString(record['first_name'] ?? record['firstName']),
3493
- middle_name: asNullableString(record['middle_name'] ?? record['middleName']),
3494
- last_name: asNullableString(record['last_name'] ?? record['lastName']),
3495
- email: asNullableString(record['email']),
3496
- phone: asNullableString(record['phone']),
3497
- unit_number: asNullableString(record['unit_number'] ?? record['unitNumber']),
3498
- referral_code: asNullableString(record['referral_code'] ?? record['referralCode']),
3499
- old_unit_number: asNullableString(record['old_unit_number'] ?? record['oldUnitNumber']),
3500
- account_email: asNullableString(record['account_email'] ?? record['accountEmail']),
3501
- two_factor: asNullableBoolean(record['two_factor'] ?? record['twoFactor']),
3502
- default_pin: asNullableBoolean(record['default_pin'] ?? record['defaultPin']),
3503
- model: asEnumMember(record['model'], USER_MODEL_TYPES),
3504
- country: mapUserCountry(record['country']),
3505
- state: mapUserStateLabel(record['state']),
3506
- email_verified_at: asNullableString(record['email_verified_at'] ?? record['emailVerifiedAt']),
3507
- phone_verified_at: asNullableString(record['phone_verified_at'] ?? record['phoneVerifiedAt']),
3508
- kyc_verified_at: asNullableString(record['kyc_verified_at'] ?? record['kycVerifiedAt']),
3509
- passport_verified_at: asNullableString(record['passport_verified_at'] ?? record['passportVerifiedAt']),
3510
- suspended_at: asNullableString(record['suspended_at'] ?? record['suspendedAt']),
3511
- deactivated_at: asNullableString(record['deactivated_at'] ?? record['deactivatedAt']),
3512
- active: asNullableBoolean(record['active']),
3513
- default_password: asNullableBoolean(record['default_password'] ?? record['defaultPassword']),
3514
- type: asEnumMember(record['type'], ACCOUNT_TYPES),
3515
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
3516
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3517
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3518
- last_login_at: asNullableString(record['last_login_at'] ?? record['lastLoginAt']),
3519
- socialite_signup: asNullableFlag01(record['socialite_signup'] ?? record['socialiteSignup']),
3520
- form_signup: asNullableFlag01(record['form_signup'] ?? record['formSignup']),
3521
- main_region: asNullableString(record['main_region'] ?? record['mainRegion']),
3522
- shipping_type: asEnumMember(record['shipping_type'] ?? record['shippingType'], SHIPPING_TYPES),
3523
- accounts: Array.isArray(record['accounts']) ? record['accounts'] : null,
3524
- business_account: mapUserBusinessAccount(record['business_account'] ?? record['businessAccount']),
3525
- account_manager: mapUserAccountManager(record['account_manager'] ?? record['accountManager']),
3526
- };
3527
- }
3528
-
3529
- /**
3530
- * Current authenticated user (`GET /user`).
3531
- *
3532
- * The backend returns a **bare** user object (no `{ success, data }` wrapper).
3533
- * {@link ApiClient} normalizes that into {@link ApiResponseModel}; this service
3534
- * maps wire fields once into {@link UserModel} (snake_case preserved).
3535
- *
3536
- * Requires an access token via {@link AuthTokenService.set}. Not cached — profile
3537
- * data is auth-sensitive and can change per session.
3538
- *
3539
- * After login, if `user.default_password` is set, send the user to
3540
- * `/onboarding/reset-password` and call {@link changePassword} (current → new).
3541
- * That page is **not** the email-link forgot-password flow.
3542
- *
3543
- * On logout, call {@link logoutFromAllSessions} while the bearer token is still
3544
- * set, then {@link AuthTokenService.clear}.
3545
- *
3546
- * @example
3547
- * ```ts
3548
- * const users = inject(UserService);
3549
- *
3550
- * users.me().subscribe((res) => {
3551
- * if (res.success) console.log(res.data?.email);
3552
- * });
3553
- * ```
3554
- */
3555
- class UserService {
3556
- api = inject(ApiClient);
3557
- /**
3558
- * Fetch the current user.
3559
- *
3560
- * @returns Normalized envelope with mapped {@link UserModel} (or `null` data).
3561
- */
3562
- me() {
3563
- return this.api.get(USER_PATH).pipe(map$1((res) => ({
3564
- ...res,
3565
- data: res.data == null ? null : mapUser(res.data),
3566
- })));
3567
- }
3568
- /**
3569
- * Change the signed-in user’s password (`POST /user/change/password`).
3570
- *
3571
- * Used on first login when {@link UserModel.default_password} is true —
3572
- * not the email-only {@link AuthService.forgot} flow.
3573
- *
3574
- * @param body - Current password plus new password and confirmation.
3575
- * @returns Normalized envelope (`data` is typically unused).
3576
- */
3577
- changePassword(body) {
3578
- return this.api.post(USER_CHANGE_PASSWORD_PATH, {
3579
- current_password: asString(body?.current_password),
3580
- password: asString(body?.password),
3581
- password_confirmation: asString(body?.password_confirmation),
3582
- });
3583
- }
3584
- /**
3585
- * Sign out of this device and every other session
3586
- * (`POST /user/logout-from-all-sessions`).
3587
- *
3588
- * Call while the bearer token is still set so the interceptor can attach
3589
- * `Authorization`. Then {@link AuthTokenService.clear} locally.
3590
- *
3591
- * @returns Normalized envelope (`data` is typically unused).
3592
- */
3593
- logoutFromAllSessions() {
3594
- return this.api.post(USER_LOGOUT_FROM_ALL_SESSIONS_PATH, {});
3595
- }
3596
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: UserService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3597
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: UserService, providedIn: 'root' });
3598
- }
3599
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: UserService, decorators: [{
3600
- type: Injectable,
3601
- args: [{ providedIn: 'root' }]
3602
- }] });
3603
-
3604
- /** User notifications read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
3605
- const NOTIFICATION_READ_PATH = '/user/notifications/read';
3606
- /** Mark read endpoint (single `{ id }` or `{}` for all). */
3607
- const NOTIFICATION_UPDATE_PATH = '/user/notifications/update';
3608
- /**
3609
- * Parse the Laravel `data` column (JSON string or object) into
3610
- * {@link NotificationPayloadModel}.
3611
- * @param raw
3612
- */
3613
- function mapNotificationPayload(raw) {
3614
- let record = asRecord(raw);
3615
- if (typeof raw === 'string' && raw.trim()) {
3616
- try {
3617
- record = asRecord(JSON.parse(raw));
3618
- }
3619
- catch {
3620
- record = null;
3621
- }
3622
- }
3623
- const payload = record ?? {};
3624
- const userId = payload['user_id'] ?? payload['userId'];
3625
- return {
3626
- user_id: userId == null ? null : asNumber(userId),
3627
- title: asString(payload['title']),
3628
- body: asNullableString(payload['body']),
3629
- link: asNullableString(payload['link']),
3630
- image: asNullableString(payload['image']),
3631
- external_link: asNullableBoolean(payload['external_link'] ?? payload['externalLink']),
3632
- };
3633
- }
3634
- /**
3635
- * Map a wire notification row into {@link NotificationModel}.
3636
- * @param raw
3637
- */
3638
- function mapNotification(raw) {
3639
- const record = asRecord(raw) ?? {};
3640
- return {
3641
- id: asString(record['id']),
3642
- type: asString(record['type']),
3643
- notifiable_type: asString(record['notifiable_type'] ?? record['notifiableType']),
3644
- notifiable_id: asNumber(record['notifiable_id'] ?? record['notifiableId']),
3645
- data: mapNotificationPayload(record['data']),
3646
- read_at: asNullableString(record['read_at'] ?? record['readAt']),
3647
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3648
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3649
- };
3650
- }
3651
- /**
3652
- * Map wire list payloads into {@link NotificationModel}[].
3653
- * @param raw
3654
- */
3655
- function mapNotificationList(raw) {
3656
- return mapList(raw, mapNotification).filter((row) => row.id !== '');
3657
- }
3658
- /**
3659
- * Derive a header/drawer inbox item from a mapped notification.
3660
- * @param notification
3661
- */
3662
- function mapNotificationInboxItem(notification) {
3663
- const row = notification ?? mapNotification(null);
3664
- const data = row.data ?? mapNotificationPayload(null);
3665
- return {
3666
- id: asString(row.id),
3667
- title: asString(data.title),
3668
- body: data.body ?? undefined,
3669
- timestamp: row.created_at ?? undefined,
3670
- read: row.read_at != null && row.read_at !== '',
3671
- link: data.link ?? undefined,
3672
- external_link: data.external_link ?? undefined,
3673
- image: data.image,
3674
- };
3675
- }
3676
-
3677
- /**
3678
- * Authenticated user notifications (`GET /user/notifications/read/{id?}`).
3679
- *
3680
- * Uses the AFRICANIES {@link ResourceId} convention for paginated / full-list reads.
3681
- * Notification primary keys are UUID strings — use {@link readOne} for a single row.
3682
- *
3683
- * Requires an access token via {@link AuthTokenService.set}. Not cached — inbox
3684
- * data is auth-sensitive and changes frequently.
3685
- *
3686
- * @example
3687
- * ```ts
3688
- * const notifications = inject(NotificationService);
3689
- *
3690
- * notifications.readAll().subscribe((res) => {
3691
- * console.log(res.data?.length);
3692
- * });
3693
- * ```
3694
- */
3695
- class NotificationService {
3696
- api = inject(ApiClient);
3697
- read(id = null, params) {
3698
- const query = id === null
3699
- ? { ...params, size: params?.size ?? NOTIFICATION_PAGE_SIZE }
3700
- : params;
3701
- return this.api
3702
- .get(buildResourcePath(NOTIFICATION_READ_PATH, id), {
3703
- params: buildResourceQueryParams(id, query),
3704
- })
3705
- .pipe(map$1((res) => ({
3706
- ...res,
3707
- data: mapResourcePayload(id, res?.data, mapNotification, mapNotificationList),
3708
- })), catchError(() => of({
3709
- success: false,
3710
- message: null,
3711
- data: [],
3712
- errors: null,
3713
- pagination: null,
3714
- status_code: null,
3715
- })));
3716
- }
3717
- /**
3718
- * Paginated page — alias for {@link read}(`null`, params).
3719
- * Size defaults to {@link NOTIFICATION_PAGE_SIZE} (`30`).
3720
- * @param params
3721
- */
3722
- readPage(params) {
3723
- return this.read(null, params);
3724
- }
3725
- /**
3726
- * Full list — alias for {@link read}(`'all'`).
3727
- * @param params
3728
- */
3729
- readAll(params) {
3730
- return this.read('all', params);
3731
- }
3732
- /**
3733
- * Single notification by UUID (`GET /user/notifications/read/{uuid}`).
3734
- * @param id
3735
- */
3736
- readOne(id) {
3737
- const trimmed = asString(id).trim();
3738
- return this.api
3739
- .get(`${NOTIFICATION_READ_PATH}/${encodeURIComponent(trimmed)}`)
3740
- .pipe(map$1((res) => ({
3741
- ...res,
3742
- data: res.data == null
3743
- ? null
3744
- : mapNotification(Array.isArray(res.data) ? res.data[0] : res.data),
3745
- })));
3746
- }
3747
- /**
3748
- * Mark one or all notifications read (`PUT /user/notifications/update`).
3749
- *
3750
- * @param id - When set, marks that notification read; omit to mark all read.
3751
- */
3752
- markRead(id) {
3753
- const body = id ? { id: asString(id) } : {};
3754
- return this.api.put(NOTIFICATION_UPDATE_PATH, body);
3755
- }
3756
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NotificationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3757
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NotificationService, providedIn: 'root' });
3758
- }
3759
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: NotificationService, decorators: [{
3760
- type: Injectable,
3761
- args: [{ providedIn: 'root' }]
3762
- }] });
3763
-
3764
- const SHIPMENT_APP_BY_MODE = {
3765
- stn: 'shiptonaija',
3766
- sfn: 'shipfromnaija',
3767
- };
3768
- /**
3769
- * Rewrite a notification portal URL for the active {@link ShippingMode}.
3770
- *
3771
- * Swaps `-export` / `-import` host segments and sets `shipment_app` to
3772
- * `shipfromnaija` (SFN) or `shiptonaija` (STN).
3773
- * @param link
3774
- * @param mode
3775
- */
3776
- function resolveNotificationLinkForMode(link, mode) {
3777
- const trimmed = asString(link).trim();
3778
- if (!trimmed) {
3779
- return trimmed;
3780
- }
3781
- try {
3782
- const url = new URL(trimmed);
3783
- url.searchParams.set('shipment_app', SHIPMENT_APP_BY_MODE[mode]);
3784
- url.hostname = rewriteNotificationHostForMode(url.hostname, mode);
3785
- return url.toString();
3786
- }
3787
- catch {
3788
- return rewriteNotificationLinkFallback(trimmed, mode);
3789
- }
3790
- }
3791
- function rewriteNotificationHostForMode(hostname, mode) {
3792
- if (mode === 'stn') {
3793
- return hostname.replace(/-export/i, '-import');
3794
- }
3795
- return hostname.replace(/-import/i, '-export');
3796
- }
3797
- function rewriteNotificationLinkFallback(link, mode) {
3798
- let out = link.replace(/shipment_app=(?:shipfromnaija|shiptonaija)/gi, `shipment_app=${SHIPMENT_APP_BY_MODE[mode]}`);
3799
- if (!/shipment_app=/i.test(out)) {
3800
- const joiner = out.includes('?') ? '&' : '?';
3801
- out = `${out}${joiner}shipment_app=${SHIPMENT_APP_BY_MODE[mode]}`;
3802
- }
3803
- if (mode === 'stn') {
3804
- return out.replace(/-export/gi, '-import');
3805
- }
3806
- return out.replace(/-import/gi, '-export');
3807
- }
3808
-
3809
- /**
3810
- * File utility reads (`POST /file/read`).
3811
- *
3812
- * Primary preview path when a record stores a `file_ref` string (shipments,
3813
- * tracking items, waybills, KYC, etc.). Body `{ ref }` → `data` with
3814
- * `mime_type` and `base_64` for `<img>` / PDF viewers.
3815
- *
3816
- * Document catalog previews use {@link DocumentService.readById} instead
3817
- * (`GET /public/document/read/{id}` → `data.file_ref.base_64`).
3818
- *
3819
- * @example
3820
- * ```ts
3821
- * const files = inject(FileService);
3822
- *
3823
- * files.read(item.file_ref).subscribe((res) => {
3824
- * if (res.success) console.log(res.data?.base_64);
3825
- * });
3826
- *
3827
- * files.readMultiple(waybillRef).subscribe((res) => {
3828
- * console.log(res.data?.map((f) => f.mime_type));
3829
- * });
3830
- * ```
3831
- */
3832
- class FileService {
3833
- api = inject(ApiClient);
3834
- /**
3835
- * Resolve one file reference (`POST /file/read`, body `{ ref }`).
3836
- *
3837
- * @param ref - Storage / document UUID from `file_ref` on a record.
3838
- * @returns Normalized envelope with mapped {@link FileReadModel}.
3839
- */
3840
- read(ref) {
3841
- return this.readByBody({ ref: asString(ref) });
3842
- }
3843
- /**
3844
- * Resolve multiple files for one ref (`POST /file/read?multiple=yes`).
3845
- *
3846
- * E-commerce waybill flows may return several pages in `data[]`.
3847
- *
3848
- * @param ref - Storage reference token.
3849
- * @returns Normalized envelope with mapped {@link FileReadModel}[].
3850
- */
3851
- readMultiple(ref) {
3852
- return this.readByBodyMultiple({ ref: asString(ref) });
3853
- }
3854
- /**
3855
- * Resolve a file from an explicit request body.
3856
- *
3857
- * @param body - Wire body (`{ ref }`).
3858
- * @returns Normalized envelope with mapped {@link FileReadModel}.
3859
- */
3860
- readByBody(body) {
3861
- return this.api
3862
- .post(FILE_READ_PATH, {
3863
- ref: asString(body?.ref),
3864
- })
3865
- .pipe(map$1((res) => ({
3866
- ...res,
3867
- data: res.data == null ? null : mapFileRead(res.data),
3868
- })));
3869
- }
3870
- /**
3871
- * Multi-file variant of {@link readByBody}.
3872
- *
3873
- * @param body - Wire body (`{ ref }`).
3874
- * @returns Normalized envelope with mapped {@link FileReadModel}[].
3875
- */
3876
- readByBodyMultiple(body) {
3877
- return this.api
3878
- .post(FILE_READ_PATH, { ref: asString(body?.ref) }, { params: { multiple: FILE_READ_MULTIPLE_PARAM } })
3879
- .pipe(map$1((res) => ({
3880
- ...res,
3881
- data: res.data == null ? null : mapFileReadList(res.data),
3882
- })));
3883
- }
3884
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3885
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FileService, providedIn: 'root' });
3886
- }
3887
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FileService, decorators: [{
3888
- type: Injectable,
3889
- args: [{ providedIn: 'root' }]
3890
- }] });
3891
-
3892
- /**
3893
- * File utility exports.
3894
- */
3895
-
3896
- /** Product read base path (relative to {@link AfricaniesSdkConfig.baseUrl}). */
3897
- const PRODUCT_READ_PATH = '/product/read';
3898
- function asNullableNumberArray(value) {
3899
- if (value == null || !Array.isArray(value)) {
3900
- return null;
3901
- }
3902
- return mapArray(value, (entry) => asNumber(entry));
3903
- }
3904
- /**
3905
- * Map a wire product into {@link ProductModel} (snake_case preserved).
3906
- * @param raw - Single product object from the API.
3907
- * @returns Normalized {@link ProductModel}.
3908
- */
3909
- function mapProduct(raw) {
3910
- const record = asRecord(raw) ?? {};
3911
- return {
3912
- id: asNumber(record['id']),
3913
- account_id: asNullableNumber(record['account_id'] ?? record['accountId']),
3914
- product_category_id: asNullableNumber(record['product_category_id'] ?? record['productCategoryId']),
3915
- hs_code: asString(record['hs_code'] ?? record['hsCode']),
3916
- hs_code_10: asNullableString(record['hs_code_10'] ?? record['hsCode10']),
3917
- hs_code_8: asNullableString(record['hs_code_8'] ?? record['hsCode8']),
3918
- hs_code_6: asNullableString(record['hs_code_6'] ?? record['hsCode6']),
3919
- name: asString(record['name']),
3920
- value: asNumber(record['value']),
3921
- usage: asNumber(record['usage']),
3922
- document_ids: asNullableNumberArray(record['document_ids'] ?? record['documentIds']),
3923
- etw_ids: asNullableNumberArray(record['etw_ids'] ?? record['etwIds']),
3924
- active: asBoolean(record['active']),
3925
- is_external: asBoolean(record['is_external'] ?? record['isExternal']),
3926
- deleted_at: asNullableString(record['deleted_at'] ?? record['deletedAt']),
3927
- created_at: asNullableString(record['created_at'] ?? record['createdAt']),
3928
- updated_at: asNullableString(record['updated_at'] ?? record['updatedAt']),
3929
- document_details: mapArray(record['document_details'] ?? record['documentDetails'], (entry) => asString(entry)),
3930
- etw_document_details: mapArray(record['etw_document_details'] ?? record['etwDocumentDetails'], (entry) => asString(entry)),
3931
- zone_product_required_documents: mapApiJsonList(record['zone_product_required_documents'] ??
3932
- record['zoneProductRequiredDocuments']),
3933
- };
3934
- }
3935
- /**
3936
- * Map a list (or single object) payload into {@link ProductModel}[].
3937
- * @param raw - `data` payload from `/product/read/{id|all}`.
3938
- * @returns Mapped product list.
3939
- */
3940
- function mapProductList(raw) {
3941
- return mapList(raw, mapProduct);
3942
- }
3943
-
3944
- /** In-memory GET cache TTL for product reference dumps / by-id (5 minutes). */
3945
- const PRODUCT_CACHE_TTL_MS = 5 * 60_000;
3946
- /**
3947
- * Product utility reads (`GET /product/read/{id?}`).
3948
- *
3949
- * Uses the AFRICANIES {@link ResourceId} convention:
3950
- * - `null` (default) → paginated page
3951
- * - `'all'` → full list
3952
- * - `number` → single {@link ProductModel}
3953
- *
3954
- * @example
3955
- * ```ts
3956
- * const products = inject(ProductService);
3957
- *
3958
- * products.readPage({ page: 1 }).subscribe((res) => {
3959
- * console.log(res.data, res.pagination);
3960
- * });
3961
- * products.readAll().subscribe((res) => console.log(res.data?.[0]?.hs_code));
3962
- * products.readById(6280).subscribe((res) => console.log(res.data?.name));
3963
- * ```
3964
- */
3965
- class ProductService {
3966
- api = inject(ApiClient);
3967
- read(id = null, params) {
3968
- return this.api
3969
- .get(buildResourcePath(PRODUCT_READ_PATH, id), {
3970
- toast: false,
3971
- params: buildResourceQueryParams(id, params),
3972
- cacheTtlMs: resourceCacheTtlMs(id, PRODUCT_CACHE_TTL_MS),
3973
- })
3974
- .pipe(map$1((res) => ({
3975
- ...res,
3976
- data: mapResourcePayload(id, res.data, mapProduct, mapProductList),
3977
- })));
3978
- }
3979
- /**
3980
- * Paginated page — alias for {@link read}(`null`, params).
3981
- * @param params
3982
- */
3983
- readPage(params) {
3984
- return this.read(null, params);
3985
- }
3986
- /**
3987
- * Full list — alias for {@link read}(`'all'`).
3988
- * @param params
3989
- */
3990
- readAll(params) {
3991
- return this.read('all', params);
3992
- }
3993
- /**
3994
- * Single record — alias for {@link read}(id).
3995
- * @param id
3996
- * @param params
3997
- */
3998
- readById(id, params) {
3999
- return this.read(id, params);
4000
- }
4001
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ProductService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4002
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ProductService, providedIn: 'root' });
4003
- }
4004
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ProductService, decorators: [{
4005
- type: Injectable,
4006
- args: [{ providedIn: 'root' }]
4007
- }] });
4008
-
4009
- /**
4010
- * Product utility exports.
4011
- */
4012
-
4013
- /**
4014
- * Select fields that declare a non-static {@link FilterOptionsSource}.
4015
- *
4016
- * @param config - Module filter schema.
4017
- * @returns Fields that need async option resolution.
4018
- */
4019
- function collectResolvableSelectFields(config) {
4020
- return asArray(config?.fields).filter((field) => field.type === 'select' &&
4021
- field.optionsSource != null &&
4022
- field.optionsSource !== 'static');
4023
- }
4024
- /**
4025
- * Unique {@link FilterOptionsSource} values referenced by a config (excluding static).
4026
- *
4027
- * @param config - Module filter schema.
4028
- */
4029
- function collectFilterOptionsSources(config) {
4030
- const sources = new Set();
4031
- for (const field of collectResolvableSelectFields(config)) {
4032
- sources.add(field.optionsSource);
4033
- }
4034
- return [...sources];
4035
- }
4036
- /**
4037
- * Map {@link WarehouseModel}[] into filter select options (`value` = id string).
4038
- *
4039
- * @param rows - Warehouses from {@link WarehouseService.readAll}.
4040
- */
4041
- function mapWarehouseFilterOptions(rows) {
4042
- if (!rows?.length) {
4043
- return [];
4044
- }
4045
- return rows.map((row) => ({
4046
- value: String(row.id),
4047
- label: row.name,
4048
- }));
4049
- }
4050
- /**
4051
- * Map {@link ShipmentMethodModel}[] into filter select options.
4052
- *
4053
- * @param rows - Methods from {@link ShipmentMethodService.readAll}.
4054
- */
4055
- function mapShipmentMethodFilterOptions(rows) {
4056
- if (!rows?.length) {
4057
- return [];
4058
- }
4059
- return rows.map((row) => ({
4060
- value: String(row.id),
4061
- label: row.name,
4062
- }));
4063
- }
4064
- /**
4065
- * Merge SDK-resolved lists with host overrides (host wins per field key).
4066
- *
4067
- * @param resolved - Output from {@link FilterOptionsResolver.resolve}.
4068
- * @param overrides - Host catalogs (e.g. manifests without a built-in service).
4069
- */
4070
- function mergeFilterOptionLists(resolved, overrides) {
4071
- if (!overrides) {
4072
- return { ...resolved };
4073
- }
4074
- return { ...resolved, ...overrides };
4075
- }
4076
-
4077
- /**
4078
- * Built-in {@link FilterOptionsSource} keys wired to SDK catalog services.
4079
- *
4080
- * `shipmentManifests` has no built-in service yet — resolve to `[]` and pass
4081
- * host `optionLists` for those field keys.
4082
- */
4083
- const SDK_FILTER_OPTIONS_SOURCES = new Set([
4084
- 'warehouses',
4085
- 'shipmentMethods',
4086
- ]);
4087
- /**
4088
- * Resolves filter drawer select options from built-in SDK catalog services.
4089
- *
4090
- * Prefer opening {@link FilterDrawerService} immediately — the drawer lazy-loads
4091
- * per field via {@link resolveField}. {@link resolve} remains for bulk prefetch.
4092
- *
4093
- * @example
4094
- * ```ts
4095
- * const resolver = inject(FilterOptionsResolver);
4096
- *
4097
- * filterDrawer.open({
4098
- * config: updateShipmentsFilterConfig,
4099
- * optionLists: { shipment_manifest_id: hostManifests },
4100
- * });
4101
- * ```
4102
- */
4103
- class FilterOptionsResolver {
4104
- warehouses = inject(WarehouseService);
4105
- shipmentMethods = inject(ShipmentMethodService);
4106
- /**
4107
- * Resolve SDK-backed options for one select field (lazy drawer load).
4108
- *
4109
- * @param field - Filter schema field with `optionsSource`.
4110
- * @returns Select rows or an error when the catalog HTTP call fails.
4111
- */
4112
- resolveField(field) {
4113
- if (field.type !== 'select' ||
4114
- field.optionsSource == null ||
4115
- field.optionsSource === 'static') {
4116
- return of(this.staticFieldOptions(field));
4117
- }
4118
- if (!SDK_FILTER_OPTIONS_SOURCES.has(field.optionsSource)) {
4119
- return of([]);
4120
- }
4121
- return this.fetchSource(field.optionsSource);
4122
- }
4123
- /**
4124
- * Resolve all SDK-backed select options for a module config (bulk prefetch).
4125
- *
4126
- * @param config - Module filter schema.
4127
- * @returns `optionLists` keyed by field.key.
4128
- */
4129
- resolve(config) {
4130
- const fields = collectResolvableSelectFields(config);
4131
- if (fields.length === 0) {
4132
- return of({});
4133
- }
4134
- const fetched = new Set();
4135
- const fetches = {};
4136
- for (const field of fields) {
4137
- const source = field.optionsSource;
4138
- if (!SDK_FILTER_OPTIONS_SOURCES.has(source) || fetched.has(source)) {
4139
- continue;
4140
- }
4141
- fetched.add(source);
4142
- fetches[source] = this.fetchSource(source).pipe(map$1((options) => {
4143
- const lists = {};
4144
- for (const f of fields) {
4145
- if (f.optionsSource === source) {
4146
- lists[f.key] = options;
4147
- }
4148
- }
4149
- return lists;
4150
- }));
4151
- }
4152
- const keys = Object.keys(fetches);
4153
- if (keys.length === 0) {
4154
- return of({});
4155
- }
4156
- return forkJoin(keys.reduce((acc, key) => {
4157
- acc[key] = fetches[key];
4158
- return acc;
4159
- }, {})).pipe(map$1((parts) => keys.reduce((merged, key) => {
4160
- Object.assign(merged, parts[key]);
4161
- return merged;
4162
- }, {})));
4163
- }
4164
- staticFieldOptions(field) {
4165
- return (field.options ?? []).map((o) => ({
4166
- value: o.value,
4167
- label: o.label,
4168
- }));
4169
- }
4170
- fetchSource(source) {
4171
- switch (source) {
4172
- case 'warehouses':
4173
- return this.warehouses.readAll().pipe(map$1((res) => res.success ? mapWarehouseFilterOptions(res.data) : []));
4174
- case 'shipmentMethods':
4175
- return this.shipmentMethods.readAll().pipe(map$1((res) => res.success ? mapShipmentMethodFilterOptions(res.data) : []));
4176
- default:
4177
- return of([]);
4178
- }
4179
- }
4180
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FilterOptionsResolver, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4181
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FilterOptionsResolver, providedIn: 'root' });
4182
- }
4183
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: FilterOptionsResolver, decorators: [{
4184
- type: Injectable,
4185
- args: [{ providedIn: 'root' }]
4186
- }] });
4187
-
4188
- /**
4189
- * Optional token for the app's modal opener (provided by `@africanies/africanies-ui`).
4190
- *
4191
- * {@link RouteOverlayService} injects this with `{ optional: true }` so apps
4192
- * can register route overlays before UI is wired — opens are skipped until
4193
- * a provider exists.
4194
- */
4195
- const MODAL_SERVICE = new InjectionToken('AFRICANIES_MODAL_SERVICE');
4196
- /**
4197
- * Optional token for the app's drawer opener (provided by `@africanies/africanies-ui`).
4198
- *
4199
- * Same optional pattern as {@link MODAL_SERVICE}.
4200
- */
4201
- const DRAWER_SERVICE = new InjectionToken('AFRICANIES_DRAWER_SERVICE');
4202
- /**
4203
- * DI token for the list of {@link OverlayRouteConfig} registered via
4204
- * {@link provideOverlayRoutes}.
4205
- */
4206
- const OVERLAY_ROUTE_CONFIGS = new InjectionToken('AFRICANIES_OVERLAY_ROUTE_CONFIGS');
4207
-
4208
- /**
4209
- * Keeps query-param state in sync with modal/drawer overlays.
4210
- *
4211
- * Instantiated eagerly via {@link provideOverlayRoutes}'s `provideAppInitializer`
4212
- * so a hard refresh with `?modal=…` already present reopens the overlay.
4213
- *
4214
- * Bidirectional sync:
4215
- * - Param appears / matches a route → open (passing sibling query params as data)
4216
- * - Param removed (e.g. browser back) → close without navigating again
4217
- * - Overlay closed by UI → strip the trigger param with `queryParamsHandling: 'merge'`
4218
- *
4219
- * Modal/drawer implementations live in `@africanies/africanies-ui` and are injected through
4220
- * {@link MODAL_SERVICE} / {@link DRAWER_SERVICE} to avoid a circular dependency.
4221
- */
4222
- class RouteOverlayService {
4223
- route = inject(ActivatedRoute);
4224
- router = inject(Router);
4225
- configs = inject(OVERLAY_ROUTE_CONFIGS);
4226
- modal = inject(MODAL_SERVICE, { optional: true });
4227
- drawer = inject(DRAWER_SERVICE, { optional: true });
4228
- openByKey = new Map();
4229
- constructor() {
4230
- // Start immediately — provideAppInitializer only forces construction.
4231
- this.route.queryParamMap.subscribe((params) => this.onQueryParams(params));
4232
- }
4233
- onQueryParams(params) {
4234
- for (const config of this.configs) {
4235
- this.syncConfig(config, params);
4236
- }
4237
- }
4238
- syncConfig(config, params) {
4239
- const value = params.get(config.paramKey);
4240
- const current = this.openByKey.get(config.paramKey);
4241
- if (value === null || value === '') {
4242
- if (current) {
4243
- current.closingFromUrl = true;
4244
- current.handle.close();
4245
- this.teardown(config.paramKey);
4246
- }
4247
- return;
4248
- }
4249
- const entry = config.routes[value];
4250
- if (!entry) {
4251
- return;
4252
- }
4253
- // Already showing the same route value — leave it alone.
4254
- if (current?.value === value) {
4255
- return;
4256
- }
4257
- if (current) {
4258
- current.closingFromUrl = true;
4259
- current.handle.close();
4260
- this.teardown(config.paramKey);
4261
- }
4262
- const opener = entry.overlay === 'modal' ? this.modal : this.drawer;
4263
- if (!opener) {
4264
- // UI package has not provided the opener yet — skip until it does.
4265
- return;
4266
- }
4267
- // Sibling query params (e.g. id=123) become OVERLAY_DATA in africanies-ui.
4268
- const data = this.siblingParams(params, config.paramKey);
4269
- const handle = opener.open(entry.component, { data });
4270
- const state = {
4271
- value,
4272
- handle,
4273
- closingFromUrl: false,
4274
- afterClosedSub: handle.afterClosed().subscribe(() => {
4275
- const open = this.openByKey.get(config.paramKey);
4276
- if (!open || open.handle !== handle) {
4277
- return;
4278
- }
4279
- const fromUrl = open.closingFromUrl;
4280
- this.teardown(config.paramKey);
4281
- if (!fromUrl) {
4282
- // Manual / UI close — drop the trigger param, keep other params.
4283
- void this.router.navigate([], {
4284
- queryParams: { [config.paramKey]: null },
4285
- queryParamsHandling: 'merge',
4286
- replaceUrl: true,
4287
- });
4288
- }
4289
- }),
4290
- };
4291
- this.openByKey.set(config.paramKey, state);
4292
- }
4293
- siblingParams(params, excludeKey) {
4294
- const data = {};
4295
- params.keys.forEach((key) => {
4296
- if (key === excludeKey) {
4297
- return;
4298
- }
4299
- const value = params.get(key);
4300
- if (value !== null) {
4301
- data[key] = value;
4302
- }
4303
- });
4304
- return data;
4305
- }
4306
- teardown(paramKey) {
4307
- const current = this.openByKey.get(paramKey);
4308
- if (!current) {
4309
- return;
4310
- }
4311
- current.afterClosedSub.unsubscribe();
4312
- this.openByKey.delete(paramKey);
4313
- }
4314
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: RouteOverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4315
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: RouteOverlayService });
4316
- }
4317
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: RouteOverlayService, decorators: [{
4318
- type: Injectable
4319
- }], ctorParameters: () => [] });
4320
-
4321
- /**
4322
- * Registers query-param → overlay maps and eagerly starts {@link RouteOverlayService}.
4323
- *
4324
- * Call beside {@link provideAfricaniesSdk} in `app.config.ts`. Modal/drawer openers
4325
- * must still be provided by `@africanies/africanies-ui` (`MODAL_SERVICE` / `DRAWER_SERVICE`).
4326
- *
4327
- * @param configs - One or more param-key namespaces (e.g. `modal` and `drawer`).
4328
- * @returns Environment providers including an app initializer.
4329
- *
4330
- * @example
4331
- * ```ts
4332
- * // app.config.ts
4333
- * import { provideRouter, Routes } from '@angular/router';
4334
- * import {
4335
- * provideAfricaniesSdk,
4336
- * provideOverlayRoutes,
4337
- * } from '@africanies/africanies-core';
4338
- * import { EditShipmentModal } from './edit-shipment.modal';
4339
- *
4340
- * export const appConfig = {
4341
- * providers: [
4342
- * provideAfricaniesSdk({ baseUrl: 'https://api.example.com' }),
4343
- * provideRouter(routes),
4344
- * provideOverlayRoutes([
4345
- * {
4346
- * paramKey: 'modal',
4347
- * routes: {
4348
- * 'edit-shipment': {
4349
- * component: EditShipmentModal,
4350
- * overlay: 'modal',
4351
- * },
4352
- * },
4353
- * },
4354
- * ]),
4355
- * // From @africanies/africanies-ui — binds MODAL_SERVICE / DRAWER_SERVICE
4356
- * // provideAfricaniesOverlays(),
4357
- * ],
4358
- * };
4359
- *
4360
- * // Template — open via query params (refresh with the same URL reopens):
4361
- * // <a routerLink="." [queryParams]="{ modal: 'edit-shipment', id: row.id }">
4362
- * // Edit
4363
- * // </a>
4364
- * ```
4365
- */
4366
- function provideOverlayRoutes(configs) {
4367
- return makeEnvironmentProviders([
4368
- { provide: OVERLAY_ROUTE_CONFIGS, useValue: configs },
4369
- RouteOverlayService,
4370
- provideAppInitializer(() => {
4371
- // Force construction so queryParamMap subscription starts at bootstrap
4372
- // (including hard refresh when the param is already present).
4373
- inject(RouteOverlayService);
4374
- }),
4375
- ]);
4376
- }
4377
-
4378
- /**
4379
- * Copy plain text to the system clipboard.
4380
- *
4381
- * Prefers `navigator.clipboard.writeText`. Falls back to a temporary
4382
- * `textarea` + `document.execCommand('copy')` when the Clipboard API is
4383
- * missing or throws (older browsers, some embedded / non-secure contexts).
4384
- *
4385
- * Safe to call during SSR — returns `false` when `document` is unavailable.
4386
- *
4387
- * @param value - Text to place on the clipboard.
4388
- * @returns `true` when the write succeeded; `false` otherwise.
4389
- *
4390
- * @example
4391
- * ```ts
4392
- * const ok = await copyToClipboard(iconName);
4393
- * if (ok) {
4394
- * toast.success(`Copied “${iconName}”`);
4395
- * }
4396
- * ```
4397
- */
4398
- async function copyToClipboard(value) {
4399
- if (typeof document === 'undefined') {
4400
- return false;
4401
- }
4402
- try {
4403
- if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
4404
- await navigator.clipboard.writeText(value);
4405
- return true;
4406
- }
4407
- }
4408
- catch {
4409
- // Fall through to the legacy execCommand path.
4410
- }
4411
- try {
4412
- const el = document.createElement('textarea');
4413
- el.value = value;
4414
- el.setAttribute('readonly', '');
4415
- el.style.position = 'fixed';
4416
- el.style.left = '-9999px';
4417
- document.body.appendChild(el);
4418
- el.select();
4419
- const ok = document.execCommand('copy');
4420
- document.body.removeChild(el);
4421
- return ok;
4422
- }
4423
- catch {
4424
- return false;
4425
- }
4426
- }
4427
-
4428
- /**
4429
- * Escape a single CSV field (RFC 4180).
4430
- *
4431
- * Quotes fields that contain commas, quotes, or line breaks. `null` /
4432
- * `undefined` become an empty cell.
4433
- *
4434
- * @param value - Cell value.
4435
- */
4436
- function csvCell(value) {
4437
- const text = value == null ? '' : String(value);
4438
- if (/[",\n\r]/.test(text)) {
4439
- return `"${text.replace(/"/g, '""')}"`;
4440
- }
4441
- return text;
4442
- }
4443
- /**
4444
- * Build a CSV document string (optional BOM + header + rows).
4445
- *
4446
- * @param options - Headers, rows, and BOM flag (filename is ignored).
4447
- */
4448
- function toCsvString(options) {
4449
- const lines = [];
4450
- if (options.headers?.length) {
4451
- lines.push(options.headers.map(csvCell).join(','));
4452
- }
4453
- for (const row of options.rows) {
4454
- lines.push(row.map(csvCell).join(','));
4455
- }
4456
- const body = lines.length ? `${lines.join('\n')}\n` : '';
4457
- const bom = options.bom === false ? '' : '\uFEFF';
4458
- return `${bom}${body}`;
4459
- }
4460
- /**
4461
- * Download a CSV file in the browser.
4462
- *
4463
- * Hosts supply filename, headers, and already-mapped row values. Safe during
4464
- * SSR — returns `false` when `document` is unavailable.
4465
- *
4466
- * @param options - Filename, headers, and row cells.
4467
- * @returns `true` when the download was triggered.
4468
- *
4469
- * @example
4470
- * ```ts
4471
- * downloadCsv({
4472
- * filename: 'warehouses.csv',
4473
- * headers: ['Name', 'Status'],
4474
- * rows: warehouses.map((row) => [
4475
- * row.name,
4476
- * row.active ? 'Active' : 'In-Active',
4477
- * ]),
4478
- * });
4479
- * ```
4480
- */
4481
- function downloadCsv(options) {
4482
- if (typeof document === 'undefined' || typeof URL === 'undefined') {
4483
- return false;
4484
- }
4485
- const blob = new Blob([toCsvString(options)], {
4486
- type: 'text/csv;charset=utf-8',
4487
- });
4488
- const url = URL.createObjectURL(blob);
4489
- const link = document.createElement('a');
4490
- link.href = url;
4491
- link.download = options.filename;
4492
- link.rel = 'noopener';
4493
- document.body.appendChild(link);
4494
- link.click();
4495
- document.body.removeChild(link);
4496
- URL.revokeObjectURL(url);
4497
- return true;
4498
- }
4499
-
4500
- /**
4501
- * Browser helpers (clipboard, CSV download, etc.) shared across host apps.
4502
- */
4503
-
4504
- // Config
4505
-
4506
- /**
4507
- * Generated bundle index. Do not edit.
4508
- */
4509
-
4510
- export { AFRICANIES_HTTP_TOAST, AFRICANIES_SDK_CONFIG, AUTH_FORGOT_PASSWORD_PATH, ApiClient, AuthService, AuthTokenService, COUNTRY_FLAG_CDN_BASE, COUNTRY_READ_PATH, CURRENCY_CREATE_PATH, CURRENCY_DELETE_PATH, CURRENCY_READ_PATH, CURRENCY_UPDATE_PATH, CountryService, CurrencyService, DOCUMENT_READ_PATH, DRAWER_SERVICE, DocumentService, FILE_READ_MULTIPLE_PARAM, FILE_READ_PATH, FileService, FilterOptionsResolver, HttpResponseCache, MODAL_SERVICE, MODE_CONFIG_PATH, ModeConfigService, NOTIFICATION_READ_PATH, NOTIFICATION_UPDATE_PATH, NotificationService, OVERLAY_ROUTE_CONFIGS, PAYMENT_METHOD_READ_PATH, PAYMENT_METHOD_UPDATE_PATH, PLAN_READ_PATH, PRODUCT_READ_PATH, PaymentMethodService, PlanService, ProductService, RouteOverlayService, SERVICE_READ_PATH, SHIPMENT_METHOD_READ_PATH, SHIPPING_MODE_OVERRIDE, ServiceService, ShipmentMethodService, ShippingModeService, TOAST_HTTP_OPTIONS, USER_CHANGE_PASSWORD_PATH, USER_LOGOUT_FROM_ALL_SESSIONS_PATH, USER_PATH, UserService, WAREHOUSE_READ_PATH, WarehouseService, ZONE_READ_PATH, ZoneService, asArray, asBoolean, asNullableBoolean, asNullableFlag01, asNullableNumber, asNullableString, asNumber, asRecord, asShippingMode$1 as asShippingMode, asString, authInterceptor, buildResourcePath, buildResourceQueryParams, collectFilterOptionsSources, collectResolvableSelectFields, copyToClipboard, countryFlagUrl, createAfricaniesQueryClientDefaults, csvCell, downloadCsv, fieldErrorsMap, formatApiErrorMessage, httpToastInterceptor, isLaravelValidationBag, isValidEmail, joinApiErrorMessages, listFetchKind, mapApiJsonList, mapApiJsonValue, mapArray, mapCountry, mapCountryList, mapCountrySelectOptions, mapCountryState, mapCurrency, mapCurrencyList, mapCurrencyPaymentMethod, mapCurrencyPaymentMethodPivot, mapDocument, mapDocumentList, mapFileRead, mapFileReadList, mapLaravelValidationBag, mapList, mapModeConfigData, mapNotification, mapNotificationInboxItem, mapNotificationList, mapNotificationPayload, mapPaymentMethod, mapPaymentMethodCurrency, mapPaymentMethodList, mapPlan, mapPlanList, mapPlanPackage, mapProduct, mapProductList, mapResourcePayload, mapService, mapServiceList, mapShipmentMethod, mapShipmentMethodFilterOptions, mapShipmentMethodList, mapShipmentMethodZoneLink, mapShipmentMethodZonePage, mapShipmentZone, mapUser, mapUserAccountManager, mapUserBusinessAccount, mapUserCountry, mapUserCountryState, mapUserGatewayPayload, mapUserPaymentPayload, mapUserPlan, mapUserPlanPackage, mapUserStateLabel, mapUserSubscription, mapWarehouse, mapWarehouseCountry, mapWarehouseFilterOptions, mapWarehouseList, mapWarehouseState, mapZone, mapZoneList, mergeFilterOptionLists, normalize, normalizePagination, provideAfricaniesHttpClient, provideAfricaniesQueryDefaults, provideAfricaniesSdk, provideModeConfig, provideOverlayRoutes, resolveModeRegionConfig, resolveNotificationLinkForMode, resourceCacheTtlMs, shipmentModeInterceptor, toCsvString, toCurrencyCreateBody, toCurrencyDeleteBody, toCurrencyFlag01, toCurrencyUpdateBody, toFlag01, toPaymentMethodFlag01, toPaymentMethodUpdateBody, unwrapLaravelPaginator, withShippingMode, withToast };
4511
- //# sourceMappingURL=africanies-africanies-core.mjs.map