@retalia/pos-components 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -8
- package/fesm2022/retalia-pos-components-services.mjs +12 -2
- package/fesm2022/retalia-pos-components-services.mjs.map +1 -1
- package/fesm2022/retalia-pos-components.mjs +267 -106
- package/fesm2022/retalia-pos-components.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/styles.css +1 -0
- package/styles/tokens.css +21 -15
- package/types/retalia-pos-components-services.d.ts +11 -2
- package/types/retalia-pos-components.d.ts +95 -23
package/README.md
CHANGED
|
@@ -27,12 +27,24 @@ import {
|
|
|
27
27
|
|
|
28
28
|
| | |
|
|
29
29
|
|---|---|
|
|
30
|
-
| `posApiInterceptor` | Reads `portalApiToken` + `contextShopId` cookies and sets `Authorization` / `Shop-Context
|
|
30
|
+
| `posApiInterceptor` | Reads `portalApiToken` + `contextShopId` cookies and sets `Authorization` / `Shop-Context`, plus `Clerk-Context` when `POS_API_CONFIG.getClerkContext` returns a clerk number. Provide via `provideHttpClient(withInterceptors([posApiInterceptor]))`. |
|
|
31
31
|
| `ClerkAuthService` | `signIn(clerkNumber, password)` — GET `/api/v1.0/clerks/{number}`, password check in the service (stub until ID-647). Call this from the host on `login.submit`. |
|
|
32
32
|
| `PosCommandsClient` | `addItem` / `changeQuantity` / `clearBasket` / `tenderCash` / `newSale` — each returns a `PosStateEnvelope`. |
|
|
33
33
|
|
|
34
34
|
In local Adaptive POS, paste a Portal Bearer into the `portalApiToken` cookie and the shop id into `contextShopId`. Do not send clerk credentials to `POST /token`.
|
|
35
35
|
|
|
36
|
+
HeadlessAPI puts the operator on the envelope `session.clerkId` from `Clerk-Context`. There is no cookie for it; the host returns the signed-in clerk from `getClerkContext` after its password check passes (null before sign-in and after sign-out):
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
{
|
|
40
|
+
provide: POS_API_CONFIG,
|
|
41
|
+
useFactory: () => {
|
|
42
|
+
const store = inject(EnvelopeStore); // wherever the host keeps the signed-in clerk
|
|
43
|
+
return { ...DEFAULT_POS_API_CONFIG, getClerkContext: () => store.clerk()?.clerkNumber ?? null };
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
36
48
|
## Styles
|
|
37
49
|
|
|
38
50
|
Add the package stylesheet once in the host app (e.g. `angular.json` `styles`):
|
|
@@ -41,7 +53,7 @@ Add the package stylesheet once in the host app (e.g. `angular.json` `styles`):
|
|
|
41
53
|
node_modules/@retalia/pos-components/styles/styles.css
|
|
42
54
|
```
|
|
43
55
|
|
|
44
|
-
Theme via CSS custom properties (`--pos-*`) defined in that file. Override tokens in the host to rebrand; values not exposed as tokens are not overridable without a library change.
|
|
56
|
+
Theme via CSS custom properties (`--pos-*`) defined in that file. Override tokens in the host to rebrand; values not exposed as tokens are not overridable without a library change. Brand wordmarks use `--pos-letter-spacing-brand`, tight titles use `--pos-letter-spacing-tight`, keypad keys use `--pos-shadow-key`, and menus use `--pos-shadow-overlay`. The masked PIN keeps `--pos-letter-spacing-display`.
|
|
45
57
|
|
|
46
58
|
Use `<pos-tokens>` (Storybook: **Theme / Tokens**, or an internal theme lab page) to see every token in action. Click a sample or token to edit values as **live overrides** — this does not change the package defaults in `styles.css`. Export a complete `:root` CSS file for the consuming POS, and import that file back into the lab later.
|
|
47
59
|
|
|
@@ -75,7 +87,7 @@ src/styles/pos-theme.css
|
|
|
75
87
|
|
|
76
88
|
## Login
|
|
77
89
|
|
|
78
|
-
Keypad login UI — presentational only. It does not call auth services.
|
|
90
|
+
Keypad login UI — presentational only. It does not call auth services.
|
|
79
91
|
|
|
80
92
|
The host envelope holds login/session state and passes it in. If the host uses a package `AuthService`, it calls that on `login.submit`, then updates `[session]` / `[errorMessage]`. The component never injects the service.
|
|
81
93
|
|
|
@@ -119,11 +131,16 @@ export class App {
|
|
|
119
131
|
| Selector | `pos-login` |
|
|
120
132
|
| Input `session` | Display-only login/session state: `signedOut` \| `busy` \| `signedIn`, optional `displayName` |
|
|
121
133
|
| Input `errorMessage` | Optional error text |
|
|
134
|
+
| Input `showAside` | Shop context beside the keypad. Default `true`. Set `false` for the keypad alone |
|
|
135
|
+
| Input `bordered` | Frame around the login shell. Default `false` |
|
|
136
|
+
| Input `showBrandImage`, `brandImage`, `brandLabel` | Same brand rules as the header. `brandLabel` defaults to `RETALIA`. Empty hides the word. Empty `brandImage` uses the built-in mark |
|
|
137
|
+
| Input `locationName`, `terminalLine`, `shopName`, `posNumber` | Defaults `Address1`, `Address2`, `shop 1`, and `001`. Empty hides that line or tile |
|
|
138
|
+
| Date | Always the PC's local date. `dateLabel` is the caption only |
|
|
122
139
|
| Output `action` | Typed intents: `login.key`, `login.next`, `login.submit`, `login.back`, … |
|
|
123
140
|
|
|
124
141
|
## Basket
|
|
125
142
|
|
|
126
|
-
Presentational sale basket — renders `basket.lines[]` and `totals` exactly as the host envelope provides them. It does not price, tax, or discount. Quantity
|
|
143
|
+
Presentational sale basket — renders `basket.lines[]` and `totals` exactly as the host envelope provides them. It does not price, tax, or discount. Quantity and void only emit intents; the host (or server) updates the envelope. Clearing the sale is a transaction action bar control, not a basket button. The host still dispatches `basket.clear`.
|
|
127
144
|
|
|
128
145
|
```ts
|
|
129
146
|
import { Component, signal } from '@angular/core';
|
|
@@ -160,7 +177,7 @@ export class Sale {
|
|
|
160
177
|
// Host envelope binding maps:
|
|
161
178
|
// basket.changeQuantity → change-quantity
|
|
162
179
|
// basket.voidLine → void-line
|
|
163
|
-
//
|
|
180
|
+
// Clear sale (`basket.clear`) is dispatched from the transaction action bar.
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
183
|
```
|
|
@@ -171,7 +188,54 @@ export class Sale {
|
|
|
171
188
|
| Input `lines` | Envelope `basket.lines[]` as given (`lineId`, `sku`, `name`, `qty`, `unitPrice`, `lineTotal`, `vat`, `struckThrough`) |
|
|
172
189
|
| Input `totals` | Envelope `totals` as given (`gross`, `net`, `vat`, `discount`, `due`, `currency`) |
|
|
173
190
|
| Input `disabled` | Optional lock while the host is busy |
|
|
174
|
-
|
|
|
191
|
+
| Input `bordered` | Panel frame. Default `false` |
|
|
192
|
+
| Output `action` | Typed intents: `basket.changeQuantity`, `basket.voidLine`. `basket.clear` remains a host intent for the action bar |
|
|
193
|
+
|
|
194
|
+
## Header
|
|
195
|
+
|
|
196
|
+
Placeable sale chrome. The host passes the title, shop or other context, and clerk name. The menu beside the clerk initial currently offers sign out only. That click emits an intent; the host clears the session and navigates. The component does not know where it sits in the layout.
|
|
197
|
+
|
|
198
|
+
```html
|
|
199
|
+
<pos-header
|
|
200
|
+
[clerkName]="clerkName()"
|
|
201
|
+
[clerkRole]="clerkRole()"
|
|
202
|
+
[disabled]="busy()"
|
|
203
|
+
(action)="onHeader($event)"
|
|
204
|
+
/>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
| | |
|
|
208
|
+
|---|---|
|
|
209
|
+
| Selector | `pos-header` |
|
|
210
|
+
| Input `showBrandImage` | Shows an image before the wordmark. Defaults to true. False hides the image, including the built-in mark |
|
|
211
|
+
| Input `brandImage` | Image URL. Empty, while the image is shown, uses the built-in Retalia mark |
|
|
212
|
+
| Input `brandLabel` | Text after the image. Empty shows no text |
|
|
213
|
+
| Input `title` | Screen title. Omitted from the bar when empty |
|
|
214
|
+
| Input `context` | Secondary label, such as the shop name |
|
|
215
|
+
| Input `clerkName` | Signed-in clerk. The bar shows an initial and the name, and hides the block when empty |
|
|
216
|
+
| Input `clerkRole` | Role label under the name, such as Clerk or Manager. Shown in lowercase |
|
|
217
|
+
| Input `signOutLabel` | Menu item label. Defaults to `Portal Logout` |
|
|
218
|
+
| Input `menuLabel` | Accessible name for the menu button. Defaults to `Menu` |
|
|
219
|
+
| Input `canSignOut` | Hides the menu button when false |
|
|
220
|
+
| Input `disabled` | Disables sign out while the host is busy |
|
|
221
|
+
| Output `action` | `header.signOut` |
|
|
222
|
+
|
|
223
|
+
## Transaction action bar
|
|
224
|
+
|
|
225
|
+
Layout container for sale commands. It does not render buttons or emit intents. The host projects the controls and decides what each one does, including mapping Cancel to `basket.clear`.
|
|
226
|
+
|
|
227
|
+
```html
|
|
228
|
+
<pos-transaction-action-bar>
|
|
229
|
+
<button type="button" (click)="onCancel()">Cancel</button>
|
|
230
|
+
<button type="button" (click)="onCash()">Cash</button>
|
|
231
|
+
</pos-transaction-action-bar>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| | |
|
|
235
|
+
|---|---|
|
|
236
|
+
| Selector | `pos-transaction-action-bar` |
|
|
237
|
+
| Content | Projected controls. The bar lays them out in a wrapping row |
|
|
238
|
+
| Output | None. Clicks stay on the projected controls |
|
|
175
239
|
|
|
176
240
|
## Item entry
|
|
177
241
|
|
|
@@ -207,13 +271,20 @@ export class Sale {
|
|
|
207
271
|
|---|---|
|
|
208
272
|
| Selector | `pos-item-entry` |
|
|
209
273
|
| Input `disabled` | Optional lock while the host is busy (e.g. AddItem in flight) |
|
|
274
|
+
| Input `bordered` | Panel frame. Default `false` |
|
|
210
275
|
| Input `errorMessage` | Optional host-provided error text (lookup failures belong to the host) |
|
|
276
|
+
| Input `label` | Accessible name for the field. Default `Item`. Not shown on the bar |
|
|
211
277
|
| Input `autofocus` | Focus the field on render so a scanner can type immediately (default `true`) |
|
|
278
|
+
| Input `placeholder` | Field hint. Default `Scan barcode, search products` |
|
|
279
|
+
| Input `scannerStatus` | Status in the chip at the right of the field. Default `scanner ready` |
|
|
280
|
+
| Input `focusHotkey` | Key that focuses the field, shown beside the chip. Default `F2`. Empty hides the hint and the shortcut |
|
|
212
281
|
| Output `action` | Typed intent: `itemEntry.submit` with `reference` |
|
|
213
282
|
|
|
283
|
+
Enter in the field submits the reference. `F2` moves focus back to the field from anywhere else on the page, matching the POS proof of concept. The label is the accessible name and is not shown on the bar.
|
|
284
|
+
|
|
214
285
|
## Tender
|
|
215
286
|
|
|
216
|
-
Presentational cash tender —
|
|
287
|
+
Presentational cash tender — coins, notes, and quick amounts build the amount given. Pay Now emits `tender.cash`. Cancel emits `tender.cancel`. The host envelope sends the cash intent and passes back `changeDue`. The component never calculates change or takes payment. Card / electronic tender is out of scope.
|
|
217
288
|
|
|
218
289
|
```ts
|
|
219
290
|
import { Component, signal } from '@angular/core';
|
|
@@ -260,7 +331,7 @@ export class Tender {
|
|
|
260
331
|
| Input `currency` | ISO currency code used only for display formatting |
|
|
261
332
|
| Input `disabled` | Optional lock while the host is busy |
|
|
262
333
|
| Input `errorMessage` | Optional error text |
|
|
263
|
-
| Output `action` |
|
|
334
|
+
| Output `action` | `tender.cash` with the amount given, or `tender.cancel` |
|
|
264
335
|
|
|
265
336
|
## Receipt
|
|
266
337
|
|
|
@@ -323,4 +394,5 @@ export class Receipt {
|
|
|
323
394
|
| Input `payments` | Envelope `payments[]` as given (`tender`, `amount`, `tendered`, `change`, `state`) |
|
|
324
395
|
| Input `document` | Envelope `document` as given (`number`, `receiptReady`) |
|
|
325
396
|
| Input `disabled` | Optional lock while the host is busy |
|
|
397
|
+
| Input `bordered` | Panel frame. Default `false` |
|
|
326
398
|
| Output `action` | Typed intent: `receipt.newSale` |
|
|
@@ -39,6 +39,9 @@ function readAccessToken(config) {
|
|
|
39
39
|
function readShopContext(config) {
|
|
40
40
|
return config.getShopContext?.() ?? readCookie(config.shopContextCookieName);
|
|
41
41
|
}
|
|
42
|
+
function readClerkContext(config) {
|
|
43
|
+
return config.getClerkContext?.()?.trim() || null;
|
|
44
|
+
}
|
|
42
45
|
|
|
43
46
|
function joinUrl(baseUrl, path) {
|
|
44
47
|
if (!baseUrl || /^https?:\/\//i.test(path)) {
|
|
@@ -48,11 +51,15 @@ function joinUrl(baseUrl, path) {
|
|
|
48
51
|
const suffix = path.replace(/^\/+/, '');
|
|
49
52
|
return `${origin}/${suffix}`;
|
|
50
53
|
}
|
|
51
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Attaches Bearer + Shop-Context from cookies (or host overrides), and Clerk-Context once the
|
|
56
|
+
* host reports a signed-in clerk.
|
|
57
|
+
*/
|
|
52
58
|
const posApiInterceptor = (req, next) => {
|
|
53
59
|
const config = inject(POS_API_CONFIG);
|
|
54
60
|
const token = readAccessToken(config);
|
|
55
61
|
const shopContext = readShopContext(config);
|
|
62
|
+
const clerkContext = readClerkContext(config);
|
|
56
63
|
let headers = req.headers;
|
|
57
64
|
if (token && !headers.has('Authorization')) {
|
|
58
65
|
headers = headers.set('Authorization', `Bearer ${token}`);
|
|
@@ -60,6 +67,9 @@ const posApiInterceptor = (req, next) => {
|
|
|
60
67
|
if (shopContext && !headers.has('Shop-Context')) {
|
|
61
68
|
headers = headers.set('Shop-Context', shopContext);
|
|
62
69
|
}
|
|
70
|
+
if (clerkContext && !headers.has('Clerk-Context')) {
|
|
71
|
+
headers = headers.set('Clerk-Context', clerkContext);
|
|
72
|
+
}
|
|
63
73
|
return next(req.clone({
|
|
64
74
|
url: joinUrl(config.apiBaseUrl, req.url),
|
|
65
75
|
headers,
|
|
@@ -189,5 +199,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
189
199
|
* Generated bundle index. Do not edit.
|
|
190
200
|
*/
|
|
191
201
|
|
|
192
|
-
export { ClerkAuthError, ClerkAuthService, DEFAULT_POS_API_CONFIG, POS_API_CONFIG, PosCommandsClient, posApiInterceptor, readAccessToken, readShopContext };
|
|
202
|
+
export { ClerkAuthError, ClerkAuthService, DEFAULT_POS_API_CONFIG, POS_API_CONFIG, PosCommandsClient, posApiInterceptor, readAccessToken, readClerkContext, readShopContext };
|
|
193
203
|
//# sourceMappingURL=retalia-pos-components-services.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retalia-pos-components-services.mjs","sources":["../../../projects/pos-components/services/read-cookie.ts","../../../projects/pos-components/services/pos-api-config.ts","../../../projects/pos-components/services/pos-api.interceptor.ts","../../../projects/pos-components/services/clerk-auth.service.ts","../../../projects/pos-components/services/pos-commands.client.ts","../../../projects/pos-components/services/retalia-pos-components-services.ts"],"sourcesContent":["/** Read a document cookie by name. Returns null when missing or not in a browser. */\r\nexport function readCookie(name: string): string | null {\r\n if (typeof document === 'undefined' || !name) {\r\n return null;\r\n }\r\n\r\n const encodedName = encodeURIComponent(name);\r\n for (const part of document.cookie.split(';')) {\r\n const trimmed = part.trim();\r\n const separator = trimmed.indexOf('=');\r\n if (separator < 0) {\r\n continue;\r\n }\r\n\r\n const key = trimmed.slice(0, separator);\r\n if (key !== name && key !== encodedName) {\r\n continue;\r\n }\r\n\r\n return decodeURIComponent(trimmed.slice(separator + 1));\r\n }\r\n\r\n return null;\r\n}\r\n","import { InjectionToken } from '@angular/core';\r\n\r\nimport { readCookie } from './read-cookie';\r\n\r\nexport interface PosApiConfig {\r\n /** Origin for HeadlessAPI. Empty string when the host app proxies `/api`. */\r\n readonly apiBaseUrl: string;\r\n /** Cookie holding the Portal Bearer token (paste in local dev). */\r\n readonly tokenCookieName: string;\r\n /** Cookie holding the shop id sent as `Shop-Context`. */\r\n readonly shopContextCookieName: string;\r\n readonly getAccessToken?: () => string | null;\r\n readonly getShopContext?: () => string | null;\r\n}\r\n\r\nexport const DEFAULT_POS_API_CONFIG: PosApiConfig = {\r\n apiBaseUrl: '',\r\n tokenCookieName: 'portalApiToken',\r\n shopContextCookieName: 'contextShopId',\r\n};\r\n\r\nexport const POS_API_CONFIG = new InjectionToken<PosApiConfig>('POS_API_CONFIG', {\r\n providedIn: 'root',\r\n factory: () => DEFAULT_POS_API_CONFIG,\r\n});\r\n\r\nexport function readAccessToken(config: PosApiConfig): string | null {\r\n return config.getAccessToken?.() ?? readCookie(config.tokenCookieName);\r\n}\r\n\r\nexport function readShopContext(config: PosApiConfig): string | null {\r\n return config.getShopContext?.() ?? readCookie(config.shopContextCookieName);\r\n}\r\n","import { HttpInterceptorFn } from '@angular/common/http';\r\nimport { inject } from '@angular/core';\r\n\r\nimport {\r\n POS_API_CONFIG,\r\n readAccessToken,\r\n readShopContext,\r\n} from './pos-api-config';\r\n\r\nfunction joinUrl(baseUrl: string, path: string): string {\r\n if (!baseUrl || /^https?:\\/\\//i.test(path)) {\r\n return path;\r\n }\r\n\r\n const origin = baseUrl.replace(/\\/+$/, '');\r\n const suffix = path.replace(/^\\/+/, '');\r\n return `${origin}/${suffix}`;\r\n}\r\n\r\n/** Attaches Bearer + Shop-Context from cookies (or host overrides). */\r\nexport const posApiInterceptor: HttpInterceptorFn = (req, next) => {\r\n const config = inject(POS_API_CONFIG);\r\n const token = readAccessToken(config);\r\n const shopContext = readShopContext(config);\r\n\r\n let headers = req.headers;\r\n if (token && !headers.has('Authorization')) {\r\n headers = headers.set('Authorization', `Bearer ${token}`);\r\n }\r\n if (shopContext && !headers.has('Shop-Context')) {\r\n headers = headers.set('Shop-Context', shopContext);\r\n }\r\n\r\n return next(\r\n req.clone({\r\n url: joinUrl(config.apiBaseUrl, req.url),\r\n headers,\r\n }),\r\n );\r\n};\r\n","import { HttpClient, HttpErrorResponse } from '@angular/common/http';\r\nimport { inject, Injectable } from '@angular/core';\r\nimport { catchError, from, Observable, switchMap, throwError } from 'rxjs';\r\n\r\n/** HeadlessAPI clerk payload. Password check is a stub until ID-647. */\r\ninterface ClerkRecord {\r\n identifier?: number;\r\n firstName?: string;\r\n lastName?: string;\r\n password?: string | null;\r\n languageCode?: string;\r\n clerkNumber?: string;\r\n isManager?: boolean;\r\n}\r\n\r\nexport interface ClerkSession {\r\n readonly clerkNumber: string;\r\n readonly displayName: string;\r\n readonly languageCode: string;\r\n readonly isManager: boolean;\r\n}\r\n\r\nexport class ClerkAuthError extends Error {\r\n override readonly name = 'ClerkAuthError';\r\n\r\n constructor(message: string) {\r\n super(message);\r\n }\r\n}\r\n\r\nconst SHA256_HEX = /^[0-9a-f]{64}$/i;\r\n\r\nfunction displayName(clerk: ClerkRecord, clerkNumber: string): string {\r\n const name = `${clerk.firstName ?? ''} ${clerk.lastName ?? ''}`.trim();\r\n return name || clerk.clerkNumber || clerkNumber;\r\n}\r\n\r\n/** HeadlessPOS `SvcClerkService.validateClerk`: SHA-256 of UTF-16 `${identifier}.${pin}`. */\r\nasync function hashClerkPin(identifier: number | undefined, pin: string): Promise<string> {\r\n const message = `${identifier}.${pin}`;\r\n const buffer = new ArrayBuffer(message.length * 2);\r\n const utf16 = new Uint16Array(buffer);\r\n for (let i = 0; i < message.length; i++) {\r\n utf16[i] = message.charCodeAt(i);\r\n }\r\n\r\n const digest = await crypto.subtle.digest('SHA-256', buffer);\r\n return Array.from(new Uint8Array(digest))\r\n .map((byte) => byte.toString(16).padStart(2, '0'))\r\n .join('')\r\n .toUpperCase();\r\n}\r\n\r\nasync function passwordsMatch(submitted: string, clerk: ClerkRecord): Promise<boolean> {\r\n const stored = clerk.password ?? '';\r\n if (SHA256_HEX.test(stored)) {\r\n const hash = await hashClerkPin(clerk.identifier, submitted);\r\n return hash === stored.toUpperCase();\r\n }\r\n\r\n return submitted === stored;\r\n}\r\n\r\n@Injectable({ providedIn: 'root' })\r\nexport class ClerkAuthService {\r\n private readonly http = inject(HttpClient);\r\n\r\n /**\r\n * Looks up the clerk and compares the keypad password in this service.\r\n * Hashed passwords use the HeadlessPOS SHA-256 check. Not a dedicated\r\n * sign-in endpoint — replace when ID-647 / PosIdentity lands.\r\n */\r\n signIn(clerkNumber: string, password: string): Observable<ClerkSession> {\r\n const number = clerkNumber.trim();\r\n if (!number) {\r\n return throwError(() => new ClerkAuthError('Invalid clerk or password'));\r\n }\r\n\r\n return this.http.get<ClerkRecord>(`/api/v1.0/clerks/${encodeURIComponent(number)}`).pipe(\r\n switchMap((clerk) => from(this.toSession(clerk, number, password))),\r\n catchError((error: unknown) => {\r\n if (error instanceof ClerkAuthError) {\r\n return throwError(() => error);\r\n }\r\n\r\n if (error instanceof HttpErrorResponse) {\r\n if (error.status === 401) {\r\n return throwError(\r\n () =>\r\n new ClerkAuthError(\r\n 'API token missing or expired. Paste portalApiToken into cookies.',\r\n ),\r\n );\r\n }\r\n\r\n if (error.status === 404) {\r\n return throwError(() => new ClerkAuthError('Invalid clerk or password'));\r\n }\r\n }\r\n\r\n return throwError(() => new ClerkAuthError('Unable to sign in'));\r\n }),\r\n );\r\n }\r\n\r\n private async toSession(\r\n clerk: ClerkRecord,\r\n clerkNumber: string,\r\n password: string,\r\n ): Promise<ClerkSession> {\r\n if (!(await passwordsMatch(password, clerk))) {\r\n throw new ClerkAuthError('Invalid clerk or password');\r\n }\r\n\r\n return {\r\n clerkNumber: clerk.clerkNumber || clerkNumber,\r\n displayName: displayName(clerk, clerkNumber),\r\n languageCode: clerk.languageCode || 'en',\r\n isManager: clerk.isManager === true,\r\n };\r\n }\r\n}\r\n","import { HttpClient } from '@angular/common/http';\r\nimport { inject, Injectable } from '@angular/core';\r\nimport { Observable } from 'rxjs';\r\n\r\nimport { PosStateEnvelope } from './pos-state-envelope';\r\n\r\nexport interface AddItemCommand {\r\n readonly basketId: string;\r\n readonly sku: string;\r\n readonly qty: number;\r\n}\r\n\r\nexport interface ChangeQuantityCommand {\r\n readonly basketId: string;\r\n readonly lineId: number;\r\n readonly qty: number;\r\n}\r\n\r\nexport interface ClearBasketCommand {\r\n readonly basketId: string;\r\n}\r\n\r\nexport interface TenderCashCommand {\r\n readonly basketId: string;\r\n readonly amount: number;\r\n}\r\n\r\nexport interface NewSaleCommand {\r\n readonly basketId: string;\r\n}\r\n\r\nfunction idempotencyKey(): string {\r\n return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;\r\n}\r\n\r\n/**\r\n * Thin HTTP client for open-till sale commands (command in → PosStateEnvelope out).\r\n *\r\n * Keep as one client while the surface is small. When basket, tender, and sale\r\n * lifecycle each grow their own handful of methods, split by subdomain, e.g.:\r\n * - basket: addItem / changeQuantity / clearBasket (+ discounts, modifiers, …)\r\n * - tender: tenderCash (+ card, void payment, …)\r\n * - sale: newSale (+ suspend, recall, …)\r\n * Extract is cheap; avoid splitting early into near-empty clients.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class PosCommandsClient {\r\n private readonly http = inject(HttpClient);\r\n\r\n addItem(\r\n command: AddItemCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>('/api/v1.0/pos/commands/add-item', command, {\r\n headers: { 'Idempotency-Key': key },\r\n });\r\n }\r\n\r\n changeQuantity(\r\n command: ChangeQuantityCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>(\r\n '/api/v1.0/pos/commands/change-quantity',\r\n command,\r\n { headers: { 'Idempotency-Key': key } },\r\n );\r\n }\r\n\r\n clearBasket(\r\n command: ClearBasketCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>(\r\n '/api/v1.0/pos/commands/clear-basket',\r\n command,\r\n { headers: { 'Idempotency-Key': key } },\r\n );\r\n }\r\n\r\n tenderCash(\r\n command: TenderCashCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>(\r\n '/api/v1.0/pos/commands/tender-cash',\r\n command,\r\n { headers: { 'Idempotency-Key': key } },\r\n );\r\n }\r\n\r\n newSale(\r\n command: NewSaleCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>(\r\n '/api/v1.0/pos/commands/new-sale',\r\n command,\r\n { headers: { 'Idempotency-Key': key } },\r\n );\r\n }\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"retalia-pos-components-services.mjs","sources":["../../../projects/pos-components/services/read-cookie.ts","../../../projects/pos-components/services/pos-api-config.ts","../../../projects/pos-components/services/pos-api.interceptor.ts","../../../projects/pos-components/services/clerk-auth.service.ts","../../../projects/pos-components/services/pos-commands.client.ts","../../../projects/pos-components/services/retalia-pos-components-services.ts"],"sourcesContent":["/** Read a document cookie by name. Returns null when missing or not in a browser. */\r\nexport function readCookie(name: string): string | null {\r\n if (typeof document === 'undefined' || !name) {\r\n return null;\r\n }\r\n\r\n const encodedName = encodeURIComponent(name);\r\n for (const part of document.cookie.split(';')) {\r\n const trimmed = part.trim();\r\n const separator = trimmed.indexOf('=');\r\n if (separator < 0) {\r\n continue;\r\n }\r\n\r\n const key = trimmed.slice(0, separator);\r\n if (key !== name && key !== encodedName) {\r\n continue;\r\n }\r\n\r\n return decodeURIComponent(trimmed.slice(separator + 1));\r\n }\r\n\r\n return null;\r\n}\r\n","import { InjectionToken } from '@angular/core';\r\n\r\nimport { readCookie } from './read-cookie';\r\n\r\nexport interface PosApiConfig {\r\n /** Origin for HeadlessAPI. Empty string when the host app proxies `/api`. */\r\n readonly apiBaseUrl: string;\r\n /** Cookie holding the Portal Bearer token (paste in local dev). */\r\n readonly tokenCookieName: string;\r\n /** Cookie holding the shop id sent as `Shop-Context`. */\r\n readonly shopContextCookieName: string;\r\n readonly getAccessToken?: () => string | null;\r\n readonly getShopContext?: () => string | null;\r\n /**\r\n * Signed-in clerk number, sent as `Clerk-Context`. There is no cookie for it: the host app\r\n * returns it once its clerk sign-in (and password check) has passed, null otherwise.\r\n */\r\n readonly getClerkContext?: () => string | null;\r\n}\r\n\r\nexport const DEFAULT_POS_API_CONFIG: PosApiConfig = {\r\n apiBaseUrl: '',\r\n tokenCookieName: 'portalApiToken',\r\n shopContextCookieName: 'contextShopId',\r\n};\r\n\r\nexport const POS_API_CONFIG = new InjectionToken<PosApiConfig>('POS_API_CONFIG', {\r\n providedIn: 'root',\r\n factory: () => DEFAULT_POS_API_CONFIG,\r\n});\r\n\r\nexport function readAccessToken(config: PosApiConfig): string | null {\r\n return config.getAccessToken?.() ?? readCookie(config.tokenCookieName);\r\n}\r\n\r\nexport function readShopContext(config: PosApiConfig): string | null {\r\n return config.getShopContext?.() ?? readCookie(config.shopContextCookieName);\r\n}\r\n\r\nexport function readClerkContext(config: PosApiConfig): string | null {\r\n return config.getClerkContext?.()?.trim() || null;\r\n}\r\n","import { HttpInterceptorFn } from '@angular/common/http';\r\nimport { inject } from '@angular/core';\r\n\r\nimport {\r\n POS_API_CONFIG,\r\n readAccessToken,\r\n readClerkContext,\r\n readShopContext,\r\n} from './pos-api-config';\r\n\r\nfunction joinUrl(baseUrl: string, path: string): string {\r\n if (!baseUrl || /^https?:\\/\\//i.test(path)) {\r\n return path;\r\n }\r\n\r\n const origin = baseUrl.replace(/\\/+$/, '');\r\n const suffix = path.replace(/^\\/+/, '');\r\n return `${origin}/${suffix}`;\r\n}\r\n\r\n/**\r\n * Attaches Bearer + Shop-Context from cookies (or host overrides), and Clerk-Context once the\r\n * host reports a signed-in clerk.\r\n */\r\nexport const posApiInterceptor: HttpInterceptorFn = (req, next) => {\r\n const config = inject(POS_API_CONFIG);\r\n const token = readAccessToken(config);\r\n const shopContext = readShopContext(config);\r\n const clerkContext = readClerkContext(config);\r\n\r\n let headers = req.headers;\r\n if (token && !headers.has('Authorization')) {\r\n headers = headers.set('Authorization', `Bearer ${token}`);\r\n }\r\n if (shopContext && !headers.has('Shop-Context')) {\r\n headers = headers.set('Shop-Context', shopContext);\r\n }\r\n if (clerkContext && !headers.has('Clerk-Context')) {\r\n headers = headers.set('Clerk-Context', clerkContext);\r\n }\r\n\r\n return next(\r\n req.clone({\r\n url: joinUrl(config.apiBaseUrl, req.url),\r\n headers,\r\n }),\r\n );\r\n};\r\n","import { HttpClient, HttpErrorResponse } from '@angular/common/http';\r\nimport { inject, Injectable } from '@angular/core';\r\nimport { catchError, from, Observable, switchMap, throwError } from 'rxjs';\r\n\r\n/** HeadlessAPI clerk payload. Password check is a stub until ID-647. */\r\ninterface ClerkRecord {\r\n identifier?: number;\r\n firstName?: string;\r\n lastName?: string;\r\n password?: string | null;\r\n languageCode?: string;\r\n clerkNumber?: string;\r\n isManager?: boolean;\r\n}\r\n\r\nexport interface ClerkSession {\r\n readonly clerkNumber: string;\r\n readonly displayName: string;\r\n readonly languageCode: string;\r\n readonly isManager: boolean;\r\n}\r\n\r\nexport class ClerkAuthError extends Error {\r\n override readonly name = 'ClerkAuthError';\r\n\r\n constructor(message: string) {\r\n super(message);\r\n }\r\n}\r\n\r\nconst SHA256_HEX = /^[0-9a-f]{64}$/i;\r\n\r\nfunction displayName(clerk: ClerkRecord, clerkNumber: string): string {\r\n const name = `${clerk.firstName ?? ''} ${clerk.lastName ?? ''}`.trim();\r\n return name || clerk.clerkNumber || clerkNumber;\r\n}\r\n\r\n/** HeadlessPOS `SvcClerkService.validateClerk`: SHA-256 of UTF-16 `${identifier}.${pin}`. */\r\nasync function hashClerkPin(identifier: number | undefined, pin: string): Promise<string> {\r\n const message = `${identifier}.${pin}`;\r\n const buffer = new ArrayBuffer(message.length * 2);\r\n const utf16 = new Uint16Array(buffer);\r\n for (let i = 0; i < message.length; i++) {\r\n utf16[i] = message.charCodeAt(i);\r\n }\r\n\r\n const digest = await crypto.subtle.digest('SHA-256', buffer);\r\n return Array.from(new Uint8Array(digest))\r\n .map((byte) => byte.toString(16).padStart(2, '0'))\r\n .join('')\r\n .toUpperCase();\r\n}\r\n\r\nasync function passwordsMatch(submitted: string, clerk: ClerkRecord): Promise<boolean> {\r\n const stored = clerk.password ?? '';\r\n if (SHA256_HEX.test(stored)) {\r\n const hash = await hashClerkPin(clerk.identifier, submitted);\r\n return hash === stored.toUpperCase();\r\n }\r\n\r\n return submitted === stored;\r\n}\r\n\r\n@Injectable({ providedIn: 'root' })\r\nexport class ClerkAuthService {\r\n private readonly http = inject(HttpClient);\r\n\r\n /**\r\n * Looks up the clerk and compares the keypad password in this service.\r\n * Hashed passwords use the HeadlessPOS SHA-256 check. Not a dedicated\r\n * sign-in endpoint — replace when ID-647 / PosIdentity lands.\r\n */\r\n signIn(clerkNumber: string, password: string): Observable<ClerkSession> {\r\n const number = clerkNumber.trim();\r\n if (!number) {\r\n return throwError(() => new ClerkAuthError('Invalid clerk or password'));\r\n }\r\n\r\n return this.http.get<ClerkRecord>(`/api/v1.0/clerks/${encodeURIComponent(number)}`).pipe(\r\n switchMap((clerk) => from(this.toSession(clerk, number, password))),\r\n catchError((error: unknown) => {\r\n if (error instanceof ClerkAuthError) {\r\n return throwError(() => error);\r\n }\r\n\r\n if (error instanceof HttpErrorResponse) {\r\n if (error.status === 401) {\r\n return throwError(\r\n () =>\r\n new ClerkAuthError(\r\n 'API token missing or expired. Paste portalApiToken into cookies.',\r\n ),\r\n );\r\n }\r\n\r\n if (error.status === 404) {\r\n return throwError(() => new ClerkAuthError('Invalid clerk or password'));\r\n }\r\n }\r\n\r\n return throwError(() => new ClerkAuthError('Unable to sign in'));\r\n }),\r\n );\r\n }\r\n\r\n private async toSession(\r\n clerk: ClerkRecord,\r\n clerkNumber: string,\r\n password: string,\r\n ): Promise<ClerkSession> {\r\n if (!(await passwordsMatch(password, clerk))) {\r\n throw new ClerkAuthError('Invalid clerk or password');\r\n }\r\n\r\n return {\r\n clerkNumber: clerk.clerkNumber || clerkNumber,\r\n displayName: displayName(clerk, clerkNumber),\r\n languageCode: clerk.languageCode || 'en',\r\n isManager: clerk.isManager === true,\r\n };\r\n }\r\n}\r\n","import { HttpClient } from '@angular/common/http';\r\nimport { inject, Injectable } from '@angular/core';\r\nimport { Observable } from 'rxjs';\r\n\r\nimport { PosStateEnvelope } from './pos-state-envelope';\r\n\r\nexport interface AddItemCommand {\r\n readonly basketId: string;\r\n readonly sku: string;\r\n readonly qty: number;\r\n}\r\n\r\nexport interface ChangeQuantityCommand {\r\n readonly basketId: string;\r\n readonly lineId: number;\r\n readonly qty: number;\r\n}\r\n\r\nexport interface ClearBasketCommand {\r\n readonly basketId: string;\r\n}\r\n\r\nexport interface TenderCashCommand {\r\n readonly basketId: string;\r\n readonly amount: number;\r\n}\r\n\r\nexport interface NewSaleCommand {\r\n readonly basketId: string;\r\n}\r\n\r\nfunction idempotencyKey(): string {\r\n return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;\r\n}\r\n\r\n/**\r\n * Thin HTTP client for open-till sale commands (command in → PosStateEnvelope out).\r\n *\r\n * Keep as one client while the surface is small. When basket, tender, and sale\r\n * lifecycle each grow their own handful of methods, split by subdomain, e.g.:\r\n * - basket: addItem / changeQuantity / clearBasket (+ discounts, modifiers, …)\r\n * - tender: tenderCash (+ card, void payment, …)\r\n * - sale: newSale (+ suspend, recall, …)\r\n * Extract is cheap; avoid splitting early into near-empty clients.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class PosCommandsClient {\r\n private readonly http = inject(HttpClient);\r\n\r\n addItem(\r\n command: AddItemCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>('/api/v1.0/pos/commands/add-item', command, {\r\n headers: { 'Idempotency-Key': key },\r\n });\r\n }\r\n\r\n changeQuantity(\r\n command: ChangeQuantityCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>(\r\n '/api/v1.0/pos/commands/change-quantity',\r\n command,\r\n { headers: { 'Idempotency-Key': key } },\r\n );\r\n }\r\n\r\n clearBasket(\r\n command: ClearBasketCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>(\r\n '/api/v1.0/pos/commands/clear-basket',\r\n command,\r\n { headers: { 'Idempotency-Key': key } },\r\n );\r\n }\r\n\r\n tenderCash(\r\n command: TenderCashCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>(\r\n '/api/v1.0/pos/commands/tender-cash',\r\n command,\r\n { headers: { 'Idempotency-Key': key } },\r\n );\r\n }\r\n\r\n newSale(\r\n command: NewSaleCommand,\r\n key = idempotencyKey(),\r\n ): Observable<PosStateEnvelope> {\r\n return this.http.post<PosStateEnvelope>(\r\n '/api/v1.0/pos/commands/new-sale',\r\n command,\r\n { headers: { 'Idempotency-Key': key } },\r\n );\r\n }\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|