@code-collective/booking-widget 1.0.6 → 1.0.7
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 +4 -0
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +1762 -2036
- package/dist/booking-widget.min.js +7 -7
- package/dist/booking-widget.umd.cjs +2 -2
- package/package.json +1 -1
- package/src/lib/CartBar.svelte +3 -1
- package/src/lib/CartOverviewButton.svelte +14 -36
- package/src/lib/Checkout.svelte +17 -0
- package/src/lib/CheckoutModal.svelte +91 -24
- package/src/lib/ContactForm.svelte +15 -6
- package/src/lib/EditBookingView.svelte +49 -4
- package/src/lib/PaymentPage.svelte +30 -25
- package/src/lib/TicketConfigurator.svelte +64 -27
- package/src/lib/api.ts +10 -2
- package/src/lib/cart-manager.ts +13 -0
- package/src/lib/config.ts +11 -8
- package/src/lib/elements/env.ts +0 -1
- package/src/lib/elements/register.ts +16 -3
- package/src/lib/index.ts +18 -7
- package/src/lib/peach-sdk.ts +40 -0
- package/src/lib/fake-api.ts +0 -282
- package/src/lib/mock-data.ts +0 -241
package/src/lib/api.ts
CHANGED
|
@@ -15,6 +15,14 @@ import type {
|
|
|
15
15
|
} from './client-types';
|
|
16
16
|
import { dateKey } from './utils';
|
|
17
17
|
|
|
18
|
+
// Lets a caller distinguish "the server said no, and specifically why" (e.g. 402 - not paid yet, retryable
|
|
19
|
+
// while Peach's webhook is still in flight) from any other failure, without parsing the message string.
|
|
20
|
+
export class ApiError extends Error {
|
|
21
|
+
constructor(public readonly status: number) {
|
|
22
|
+
super(`API error ${status}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
export interface BookingApi {
|
|
19
27
|
sessionToken: string;
|
|
20
28
|
cartToken: string;
|
|
@@ -70,7 +78,7 @@ export class ApiClient implements BookingApi {
|
|
|
70
78
|
|
|
71
79
|
private unwrap<T>(result: { data?: T; error?: unknown; response: Response }): T {
|
|
72
80
|
if (result.data === undefined) {
|
|
73
|
-
throw new
|
|
81
|
+
throw new ApiError(result.response.status);
|
|
74
82
|
}
|
|
75
83
|
return result.data;
|
|
76
84
|
}
|
|
@@ -185,7 +193,7 @@ export class ApiClient implements BookingApi {
|
|
|
185
193
|
params: { path: { itemId } },
|
|
186
194
|
});
|
|
187
195
|
if (result.response.status >= 400) {
|
|
188
|
-
throw new
|
|
196
|
+
throw new ApiError(result.response.status);
|
|
189
197
|
}
|
|
190
198
|
}
|
|
191
199
|
|
package/src/lib/cart-manager.ts
CHANGED
|
@@ -34,6 +34,19 @@ export class CartManager {
|
|
|
34
34
|
return cart;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// The client only knows a cart's absolute/idle expiry from whenever it was last (re)created here - it
|
|
38
|
+
// never sees the server's sliding idle window update in between, so a cart the server has since rejected
|
|
39
|
+
// (e.g. idle timeout) can still look valid locally. Callers use this to drop that stale state and force
|
|
40
|
+
// ensureCart to mint a fresh one after the server refuses an operation on the cart it was given.
|
|
41
|
+
reset(): void {
|
|
42
|
+
this.cart = null;
|
|
43
|
+
try {
|
|
44
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
45
|
+
} catch {
|
|
46
|
+
// localStorage unavailable (e.g. iframe sandbox)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
private isExpired(): boolean {
|
|
38
51
|
if (!this.cart) return true;
|
|
39
52
|
return new Date(this.cart.absoluteExpiresAt ?? 0).getTime() <= Date.now();
|
package/src/lib/config.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Defaults for the standalone dev app (App.svelte) when no query params are given at all, or no apiBaseUrl
|
|
2
|
+
// is given - same real QA product booking/demo/product.html's own fallback attributes point at, so `npm run
|
|
3
|
+
// dev` with no params still shows something real instead of talking to an empty relative URL.
|
|
4
|
+
const DEFAULT_QA_API_BASE_URL = 'https://qa.tranzact.co.za/morii-checkout/api';
|
|
5
|
+
const DEFAULT_QA_PRODUCT_ID = 'fec72dc6-0357-411b-88f5-d37083ab88cb';
|
|
6
|
+
const DEFAULT_QA_CHECKOUT_KEY = 'morii_checkout_key_2qGS3onnv2eigOiV6RT1yQ';
|
|
7
|
+
|
|
1
8
|
export type WidgetMode = 'configurator' | 'checkout' | 'cart-overview';
|
|
2
9
|
|
|
3
10
|
export type CartOverviewDisplay = 'bar' | 'button';
|
|
@@ -81,7 +88,6 @@ export interface AppConfig {
|
|
|
81
88
|
productId: string;
|
|
82
89
|
checkoutKey: string;
|
|
83
90
|
currency: string;
|
|
84
|
-
useFakeApi: boolean;
|
|
85
91
|
mode: WidgetMode;
|
|
86
92
|
cartDisplay: CartOverviewDisplay;
|
|
87
93
|
orderId: string;
|
|
@@ -96,14 +102,12 @@ export function readConfig(): AppConfig {
|
|
|
96
102
|
const params = new URLSearchParams(window.location.search);
|
|
97
103
|
|
|
98
104
|
if (params.has('product') || params.has('mode')) {
|
|
99
|
-
const apiBaseUrl = params.get('apiBaseUrl') ?? '';
|
|
100
105
|
const productId = params.get('product') ?? '';
|
|
101
106
|
return {
|
|
102
|
-
apiBaseUrl,
|
|
107
|
+
apiBaseUrl: params.get('apiBaseUrl') ?? DEFAULT_QA_API_BASE_URL,
|
|
103
108
|
productId,
|
|
104
109
|
checkoutKey: params.get('checkoutKey') ?? deriveCheckoutKey(productId),
|
|
105
110
|
currency: params.get('currency') ?? 'ZAR',
|
|
106
|
-
useFakeApi: params.get('useFakeApi') === 'true' || !apiBaseUrl,
|
|
107
111
|
mode: parseMode(params.get('mode')),
|
|
108
112
|
cartDisplay: parseCartDisplay(params.get('cartDisplay')),
|
|
109
113
|
orderId: params.get('orderId') ?? '',
|
|
@@ -116,11 +120,10 @@ export function readConfig(): AppConfig {
|
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
return {
|
|
119
|
-
apiBaseUrl:
|
|
120
|
-
productId:
|
|
121
|
-
checkoutKey:
|
|
123
|
+
apiBaseUrl: DEFAULT_QA_API_BASE_URL,
|
|
124
|
+
productId: DEFAULT_QA_PRODUCT_ID,
|
|
125
|
+
checkoutKey: DEFAULT_QA_CHECKOUT_KEY,
|
|
122
126
|
currency: 'ZAR',
|
|
123
|
-
useFakeApi: true,
|
|
124
127
|
mode: 'configurator',
|
|
125
128
|
cartDisplay: 'bar',
|
|
126
129
|
orderId: '',
|
package/src/lib/elements/env.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import '../app.css';
|
|
2
2
|
import type { BookingApi } from '../api';
|
|
3
3
|
import { ApiClient } from '../api';
|
|
4
|
-
import { FakeApiClient } from '../fake-api';
|
|
5
4
|
import { SessionManager } from '../session-manager';
|
|
6
5
|
import { CartManager } from '../cart-manager';
|
|
7
|
-
import { defaultApiBaseUrl
|
|
6
|
+
import { defaultApiBaseUrl } from './env';
|
|
8
7
|
|
|
9
8
|
// Bootstrap shared services BEFORE element imports trigger connectedCallback.
|
|
10
9
|
// Elements read from window.__bwServices instead of importing shared.ts,
|
|
11
10
|
// because the IIFE bundler inlines imports per-element — a module-level
|
|
12
11
|
// singleton would be duplicated. window is the only truly shared scope.
|
|
13
12
|
const checkoutKey = document.querySelector('[checkout-key]')?.getAttribute('checkout-key') ?? '';
|
|
14
|
-
const api: BookingApi =
|
|
13
|
+
const api: BookingApi = new ApiClient(defaultApiBaseUrl);
|
|
15
14
|
const sessionManager = new SessionManager(api);
|
|
16
15
|
const cartManager = new CartManager(api);
|
|
17
16
|
sessionManager.startBackgroundRefresh();
|
|
@@ -22,6 +21,20 @@ import './bw-configurator.svelte';
|
|
|
22
21
|
import './bw-cart.svelte';
|
|
23
22
|
import './bw-checkout.svelte';
|
|
24
23
|
|
|
24
|
+
// Cart-global, not tied to any one element (unlike the bw:* events forwarded per-element in autoWire below),
|
|
25
|
+
// so this is wired once here rather than per bw-configurator/bw-cart/bw-checkout instance. Carries the same
|
|
26
|
+
// cart TicketConfigurator/CheckoutModal already fetch for their own posting - see those files' own comments -
|
|
27
|
+
// so a consumer can build a custom cart summary (item count, remaining time, item details) without calling
|
|
28
|
+
// the checkout API directly.
|
|
29
|
+
window.addEventListener('message', (e) => {
|
|
30
|
+
let d: Record<string, unknown>;
|
|
31
|
+
try { d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; }
|
|
32
|
+
catch { return; }
|
|
33
|
+
if (d?.type === 'cart:updated' && 'cart' in d) {
|
|
34
|
+
window.dispatchEvent(new CustomEvent('bw:cart-updated', { detail: { cart: d.cart } }));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
25
38
|
interface BwOptions {
|
|
26
39
|
shouldBottomCloseOnModal: boolean;
|
|
27
40
|
autoSelectSingleTimeSlot: boolean;
|
package/src/lib/index.ts
CHANGED
|
@@ -2,8 +2,8 @@ import './app.css';
|
|
|
2
2
|
import { mount, unmount } from 'svelte';
|
|
3
3
|
import type { WizardPages, CartOverviewDisplay } from './config';
|
|
4
4
|
import type { BookingApi } from './api';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import type { CheckoutCartDetailDto } from './client-types';
|
|
6
|
+
import { ApiClient, ApiError } from './api';
|
|
7
7
|
import { SessionManager } from './session-manager';
|
|
8
8
|
import { CartManager } from './cart-manager';
|
|
9
9
|
import TicketConfiguratorComponent from './TicketConfigurator.svelte';
|
|
@@ -18,7 +18,7 @@ export { default as CartBar } from './CartBar.svelte';
|
|
|
18
18
|
export { default as CartOverviewButton } from './CartOverviewButton.svelte';
|
|
19
19
|
|
|
20
20
|
// Re-export utilities
|
|
21
|
-
export { ApiClient,
|
|
21
|
+
export { ApiClient, ApiError, SessionManager, CartManager };
|
|
22
22
|
export { resolveWizardPages, resolveEditPages, DEFAULT_WIZARD_PAGES } from './config';
|
|
23
23
|
export type { BookingApi } from './api';
|
|
24
24
|
export type { WizardPages, WizardPageConfig, CartOverviewDisplay, WidgetMode, WizardWidgetType } from './config';
|
|
@@ -30,7 +30,11 @@ export type * from './client-types';
|
|
|
30
30
|
interface BaseConfig {
|
|
31
31
|
apiBaseUrl?: string;
|
|
32
32
|
checkoutKey?: string;
|
|
33
|
-
|
|
33
|
+
// Cart-global, not tied to any one mount function - fires after any add/edit/remove, anywhere on the page,
|
|
34
|
+
// carrying the same cart TicketConfigurator/CheckoutModal already fetch for their own internal use. Lets a
|
|
35
|
+
// consumer build a custom cart summary (item count, remaining time, item details) without calling the
|
|
36
|
+
// checkout API directly.
|
|
37
|
+
onCartUpdated?: (cart: CheckoutCartDetailDto | null) => void;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export interface ConfiguratorConfig extends BaseConfig {
|
|
@@ -63,9 +67,7 @@ export interface MountedWidget {
|
|
|
63
67
|
// --- Shared bootstrap ---
|
|
64
68
|
|
|
65
69
|
function bootstrap(config: BaseConfig): { api: BookingApi; sessionManager: SessionManager; cartManager: CartManager } {
|
|
66
|
-
const api: BookingApi = config.
|
|
67
|
-
? new FakeApiClient()
|
|
68
|
-
: new ApiClient(config.apiBaseUrl ?? '');
|
|
70
|
+
const api: BookingApi = new ApiClient(config.apiBaseUrl ?? '');
|
|
69
71
|
|
|
70
72
|
const sessionManager = new SessionManager(api);
|
|
71
73
|
const cartManager = new CartManager(api);
|
|
@@ -107,6 +109,9 @@ export async function mountConfigurator(target: HTMLElement, config: Configurato
|
|
|
107
109
|
totalFormatted: (d.totalFormatted as string) ?? '',
|
|
108
110
|
});
|
|
109
111
|
}
|
|
112
|
+
if (d.type === 'cart:updated' && 'cart' in d) {
|
|
113
|
+
config.onCartUpdated?.(d.cart as CheckoutCartDetailDto | null);
|
|
114
|
+
}
|
|
110
115
|
}
|
|
111
116
|
window.addEventListener('message', messageHandler);
|
|
112
117
|
|
|
@@ -148,6 +153,9 @@ export async function mountCheckout(target: HTMLElement, config: CheckoutConfig)
|
|
|
148
153
|
currency: (d.currency as string) ?? 'ZAR',
|
|
149
154
|
});
|
|
150
155
|
}
|
|
156
|
+
if (d.type === 'cart:updated' && 'cart' in d) {
|
|
157
|
+
config.onCartUpdated?.(d.cart as CheckoutCartDetailDto | null);
|
|
158
|
+
}
|
|
151
159
|
}
|
|
152
160
|
window.addEventListener('message', messageHandler);
|
|
153
161
|
|
|
@@ -181,6 +189,9 @@ export async function mountCartOverview(target: HTMLElement, config: CartOvervie
|
|
|
181
189
|
if (!d?.type) return;
|
|
182
190
|
|
|
183
191
|
if (d.type === 'modal:open' && config.onCheckout) config.onCheckout();
|
|
192
|
+
if (d.type === 'cart:updated' && 'cart' in d) {
|
|
193
|
+
config.onCartUpdated?.(d.cart as CheckoutCartDetailDto | null);
|
|
194
|
+
}
|
|
184
195
|
}
|
|
185
196
|
window.addEventListener('message', messageHandler);
|
|
186
197
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const SDK_URLS: Record<string, string> = {
|
|
2
|
+
prod: 'https://checkout.peachpayments.com/js/checkout.js',
|
|
3
|
+
qa: 'https://sandbox-checkout.peachpayments.com/js/checkout.js',
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
let loadPromise: Promise<void> | null = null;
|
|
7
|
+
|
|
8
|
+
// Lazily injects Peach's Copy&Pay SDK the first time PaymentPage mounts, so consumers embedding the widget
|
|
9
|
+
// (any of the three integration options) never have to know this script exists, let alone which
|
|
10
|
+
// environment's URL to point at - same window.BW_CHECKOUT_ENV convention elements/env.ts already uses for
|
|
11
|
+
// the checkout API base URL, with an explicit override for anything that doesn't fit that pattern.
|
|
12
|
+
export function loadPeachSdk(): Promise<void> {
|
|
13
|
+
if ((window as any).Checkout) {
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
}
|
|
16
|
+
if (loadPromise) {
|
|
17
|
+
return loadPromise;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const w = window as any;
|
|
21
|
+
const env: string = w.BW_CHECKOUT_ENV ?? 'prod';
|
|
22
|
+
const src: string = w.BW_CHECKOUT_PEACH_SDK_URL ?? SDK_URLS[env] ?? SDK_URLS.prod;
|
|
23
|
+
|
|
24
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
25
|
+
const existing = document.querySelector(`script[src="${src}"]`);
|
|
26
|
+
if (existing) {
|
|
27
|
+
existing.addEventListener('load', () => resolve());
|
|
28
|
+
existing.addEventListener('error', () => reject(new Error('Failed to load the Peach Payments SDK script')));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const script = document.createElement('script');
|
|
33
|
+
script.src = src;
|
|
34
|
+
script.onload = () => resolve();
|
|
35
|
+
script.onerror = () => reject(new Error('Failed to load the Peach Payments SDK script'));
|
|
36
|
+
document.head.appendChild(script);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return loadPromise;
|
|
40
|
+
}
|
package/src/lib/fake-api.ts
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CheckoutAddCartItemDto,
|
|
3
|
-
CheckoutSessionDto,
|
|
4
|
-
CheckoutProductDto,
|
|
5
|
-
CheckoutAvailabilityCalendarDto,
|
|
6
|
-
CheckoutAvailabilityDto,
|
|
7
|
-
CheckoutCartResult,
|
|
8
|
-
CheckoutCartDetailDto,
|
|
9
|
-
CheckoutCartItemDetailDto,
|
|
10
|
-
CheckoutCartItemAddedDto,
|
|
11
|
-
CheckoutCartPaymentInitiationDto,
|
|
12
|
-
CheckoutCartConfirmResultDto,
|
|
13
|
-
CheckoutCartConfirmItemResultDto,
|
|
14
|
-
OctoContact,
|
|
15
|
-
} from './client-types';
|
|
16
|
-
import type { BookingApi } from './api';
|
|
17
|
-
import {
|
|
18
|
-
sampleProductData,
|
|
19
|
-
generateSampleAvailability,
|
|
20
|
-
generateSampleTimeSlots,
|
|
21
|
-
} from './mock-data';
|
|
22
|
-
|
|
23
|
-
function delay<T>(ms: number, fn: () => T): Promise<T> {
|
|
24
|
-
return new Promise((r) => setTimeout(() => r(fn()), ms));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function uuid(): string {
|
|
28
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
29
|
-
const r = (Math.random() * 16) | 0;
|
|
30
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
31
|
-
return v.toString(16);
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface FakeCartItem {
|
|
36
|
-
id: string;
|
|
37
|
-
bookingUuid: string;
|
|
38
|
-
request: CheckoutAddCartItemDto;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const CART_STORAGE_KEY = 'morii-checkout-cart';
|
|
42
|
-
const ITEMS_STORAGE_KEY = 'morii-checkout-cart-items';
|
|
43
|
-
|
|
44
|
-
export class FakeApiClient implements BookingApi {
|
|
45
|
-
sessionToken = '';
|
|
46
|
-
cartToken = '';
|
|
47
|
-
private cartItems: FakeCartItem[] = [];
|
|
48
|
-
|
|
49
|
-
constructor() {
|
|
50
|
-
this.loadCartFromStorage() || this.seedCart();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private seedCart(): void {
|
|
54
|
-
this.cartToken = `fake-cart-seeded`;
|
|
55
|
-
|
|
56
|
-
this.cartItems = [
|
|
57
|
-
{
|
|
58
|
-
id: '00000000-0000-0000-0000-000000000001',
|
|
59
|
-
bookingUuid: '00000000-0000-0000-0000-aaaaaaaaaaaa',
|
|
60
|
-
request: {
|
|
61
|
-
productId: sampleProductData.id,
|
|
62
|
-
optionId: 'option_blue_tram',
|
|
63
|
-
unitItems: [{ unitId: 'unit_adult' }, { unitId: 'unit_adult' }, { unitId: 'unit_child' }],
|
|
64
|
-
availabilityId: 'avail_seeded',
|
|
65
|
-
amount: 202500,
|
|
66
|
-
currencyCode: 'ZAR',
|
|
67
|
-
currencyPrecision: 2,
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
this.saveCartToStorage();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private saveCartToStorage(): void {
|
|
76
|
-
try {
|
|
77
|
-
const now = new Date();
|
|
78
|
-
const absolute = new Date(now.getTime() + 60 * 60_000);
|
|
79
|
-
localStorage.setItem(CART_STORAGE_KEY, JSON.stringify({
|
|
80
|
-
cartToken: this.cartToken,
|
|
81
|
-
absoluteExpiresAt: absolute.toISOString(),
|
|
82
|
-
}));
|
|
83
|
-
localStorage.setItem(ITEMS_STORAGE_KEY, JSON.stringify(this.cartItems));
|
|
84
|
-
} catch {
|
|
85
|
-
// localStorage unavailable
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private loadCartFromStorage(): boolean {
|
|
90
|
-
try {
|
|
91
|
-
const cartRaw = localStorage.getItem(CART_STORAGE_KEY);
|
|
92
|
-
const itemsRaw = localStorage.getItem(ITEMS_STORAGE_KEY);
|
|
93
|
-
if (!cartRaw || !itemsRaw) return false;
|
|
94
|
-
|
|
95
|
-
const cart = JSON.parse(cartRaw);
|
|
96
|
-
if (new Date(cart.absoluteExpiresAt).getTime() <= Date.now()) return false;
|
|
97
|
-
|
|
98
|
-
this.cartToken = cart.cartToken;
|
|
99
|
-
this.cartItems = JSON.parse(itemsRaw);
|
|
100
|
-
return true;
|
|
101
|
-
} catch {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// --- Session ---
|
|
107
|
-
|
|
108
|
-
async startSession(_checkoutKey: string): Promise<CheckoutSessionDto> {
|
|
109
|
-
return delay(300, () => {
|
|
110
|
-
const now = new Date();
|
|
111
|
-
const expires = new Date(now.getTime() + 30 * 60_000);
|
|
112
|
-
const refresh = new Date(now.getTime() + 20 * 60_000);
|
|
113
|
-
this.sessionToken = `fake-session-${uuid().slice(0, 8)}`;
|
|
114
|
-
const session: CheckoutSessionDto = {
|
|
115
|
-
sessionToken: this.sessionToken,
|
|
116
|
-
issuedAt: now.toISOString(),
|
|
117
|
-
expiresAt: expires.toISOString(),
|
|
118
|
-
refreshAfter: refresh.toISOString(),
|
|
119
|
-
octo: {
|
|
120
|
-
basePath: '/v1/checkout/octo',
|
|
121
|
-
capabilities: 'pricing,content,pickups',
|
|
122
|
-
},
|
|
123
|
-
supplier: {
|
|
124
|
-
name: 'Franschhoek Wine Tram',
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
return session;
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async refreshSession(): Promise<CheckoutSessionDto> {
|
|
132
|
-
return this.startSession('');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// --- Catalog ---
|
|
136
|
-
|
|
137
|
-
async getProduct(productId: string): Promise<CheckoutProductDto> {
|
|
138
|
-
return delay(500, () => ({ ...sampleProductData, id: productId }));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async getAvailabilityCalendar(
|
|
142
|
-
_productId: string,
|
|
143
|
-
_optionId: string,
|
|
144
|
-
): Promise<Record<string, CheckoutAvailabilityCalendarDto>> {
|
|
145
|
-
return delay(600, () => generateSampleAvailability());
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async getAvailability(
|
|
149
|
-
_productId: string,
|
|
150
|
-
_optionId: string,
|
|
151
|
-
localDate: string,
|
|
152
|
-
): Promise<CheckoutAvailabilityDto[]> {
|
|
153
|
-
return delay(400, () => {
|
|
154
|
-
const parts = localDate.split('-').map(Number);
|
|
155
|
-
const date = new Date(parts[0], parts[1] - 1, parts[2]);
|
|
156
|
-
return generateSampleTimeSlots(date);
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// --- Cart ---
|
|
161
|
-
|
|
162
|
-
async createCart(): Promise<CheckoutCartResult> {
|
|
163
|
-
return delay(300, () => {
|
|
164
|
-
const now = new Date();
|
|
165
|
-
const idle = new Date(now.getTime() + 15 * 60_000);
|
|
166
|
-
const absolute = new Date(now.getTime() + 60 * 60_000);
|
|
167
|
-
|
|
168
|
-
if (this.cartToken && this.cartItems.length > 0) {
|
|
169
|
-
return {
|
|
170
|
-
cartToken: this.cartToken,
|
|
171
|
-
issuedAt: now.toISOString(),
|
|
172
|
-
idleExpiresAt: idle.toISOString(),
|
|
173
|
-
absoluteExpiresAt: absolute.toISOString(),
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
this.cartToken = `fake-cart-${uuid().slice(0, 8)}`;
|
|
178
|
-
this.cartItems = [];
|
|
179
|
-
this.saveCartToStorage();
|
|
180
|
-
return {
|
|
181
|
-
cartToken: this.cartToken,
|
|
182
|
-
issuedAt: now.toISOString(),
|
|
183
|
-
idleExpiresAt: idle.toISOString(),
|
|
184
|
-
absoluteExpiresAt: absolute.toISOString(),
|
|
185
|
-
};
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async getCart(): Promise<CheckoutCartDetailDto> {
|
|
190
|
-
return delay(300, () => {
|
|
191
|
-
const now = new Date();
|
|
192
|
-
const idle = new Date(now.getTime() + 15 * 60_000);
|
|
193
|
-
const absolute = new Date(now.getTime() + 60 * 60_000);
|
|
194
|
-
const detail: CheckoutCartDetailDto = {
|
|
195
|
-
cartToken: this.cartToken,
|
|
196
|
-
issuedAt: now.toISOString(),
|
|
197
|
-
idleExpiresAt: idle.toISOString(),
|
|
198
|
-
absoluteExpiresAt: absolute.toISOString(),
|
|
199
|
-
items: this.cartItems.map((item): CheckoutCartItemDetailDto => ({
|
|
200
|
-
id: item.id,
|
|
201
|
-
bookingUuid: item.bookingUuid,
|
|
202
|
-
productId: item.request.productId,
|
|
203
|
-
optionId: item.request.optionId,
|
|
204
|
-
unitItems: item.request.unitItems,
|
|
205
|
-
availabilityId: item.request.availabilityId,
|
|
206
|
-
localDate: item.request.localDate,
|
|
207
|
-
pickupPointId: item.request.pickupPointId,
|
|
208
|
-
amount: item.request.amount,
|
|
209
|
-
currencyCode: item.request.currencyCode,
|
|
210
|
-
currencyPrecision: item.request.currencyPrecision,
|
|
211
|
-
})),
|
|
212
|
-
};
|
|
213
|
-
return detail;
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async addCartItem(request: CheckoutAddCartItemDto): Promise<CheckoutCartItemAddedDto> {
|
|
218
|
-
return delay(400, () => {
|
|
219
|
-
const item: FakeCartItem = {
|
|
220
|
-
id: uuid(),
|
|
221
|
-
bookingUuid: uuid(),
|
|
222
|
-
request,
|
|
223
|
-
};
|
|
224
|
-
this.cartItems.push(item);
|
|
225
|
-
this.saveCartToStorage();
|
|
226
|
-
return { id: item.id, bookingUuid: item.bookingUuid };
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async updateCartItem(itemId: string, request: CheckoutAddCartItemDto): Promise<CheckoutCartItemAddedDto> {
|
|
231
|
-
return delay(400, () => {
|
|
232
|
-
const existing = this.cartItems.find((i) => i.id === itemId);
|
|
233
|
-
if (existing) {
|
|
234
|
-
existing.request = request;
|
|
235
|
-
this.saveCartToStorage();
|
|
236
|
-
return { id: existing.id, bookingUuid: existing.bookingUuid };
|
|
237
|
-
}
|
|
238
|
-
const item: FakeCartItem = { id: itemId, bookingUuid: uuid(), request };
|
|
239
|
-
this.cartItems.push(item);
|
|
240
|
-
this.saveCartToStorage();
|
|
241
|
-
return { id: item.id, bookingUuid: item.bookingUuid };
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async removeCartItem(itemId: string): Promise<void> {
|
|
246
|
-
return delay(200, () => {
|
|
247
|
-
this.cartItems = this.cartItems.filter((i) => i.id !== itemId);
|
|
248
|
-
this.saveCartToStorage();
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async payCart(_contact: OctoContact): Promise<CheckoutCartPaymentInitiationDto> {
|
|
253
|
-
return delay(800, () => ({
|
|
254
|
-
checkoutId: `fake-checkout-${uuid().slice(0, 8)}`,
|
|
255
|
-
redirectUrl: '',
|
|
256
|
-
}));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async confirmCart(): Promise<CheckoutCartConfirmResultDto> {
|
|
260
|
-
return delay(700, () => {
|
|
261
|
-
const result: CheckoutCartConfirmResultDto = {
|
|
262
|
-
items: this.cartItems.map((item): CheckoutCartConfirmItemResultDto => ({
|
|
263
|
-
bookingUuid: item.bookingUuid,
|
|
264
|
-
statusCode: 200,
|
|
265
|
-
body: JSON.stringify({
|
|
266
|
-
id: `booking-${item.bookingUuid.slice(0, 8)}`,
|
|
267
|
-
uuid: item.bookingUuid,
|
|
268
|
-
status: 'CONFIRMED',
|
|
269
|
-
productId: item.request.productId,
|
|
270
|
-
optionId: item.request.optionId,
|
|
271
|
-
unitItems: (item.request.unitItems ?? []).map((u) => ({
|
|
272
|
-
uuid: uuid(),
|
|
273
|
-
unitId: u.unitId,
|
|
274
|
-
status: 'CONFIRMED',
|
|
275
|
-
})),
|
|
276
|
-
}),
|
|
277
|
-
})),
|
|
278
|
-
};
|
|
279
|
-
return result;
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
}
|