@hanzo/commerce 7.1.34 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,34 +0,0 @@
1
- import { reaction } from 'mobx'
2
- import StandaloneService, { type StandaloneServiceSnapshot } from './standalone-service'
3
-
4
- const LS_KEY = 'lux-cart'
5
-
6
- const readSnapshot = (): StandaloneServiceSnapshot | undefined => {
7
- const snapshotAsStr = localStorage.getItem(LS_KEY)
8
- return snapshotAsStr ? JSON.parse(snapshotAsStr) as StandaloneServiceSnapshot : undefined
9
- }
10
-
11
- const listenAndWriteSnapshots = (cmmc: StandaloneService): void => {
12
-
13
- if (typeof window !== 'undefined') {
14
- reaction(
15
- () => (cmmc.cartTotal),
16
- (total) => {
17
- if (total > 0) {
18
- const snapshot = cmmc.takeSnapshot()
19
- // console.log(`CMMC LOCAL STORAGE UPDATE. (CART TOTAL: ${total}`)
20
- localStorage.setItem(LS_KEY, JSON.stringify(snapshot) )
21
- }
22
- else {
23
- localStorage.removeItem(LS_KEY)
24
- }
25
- }
26
- )
27
- }
28
- }
29
-
30
- export {
31
- readSnapshot,
32
- listenAndWriteSnapshots
33
- }
34
-
@@ -1,550 +0,0 @@
1
- import {
2
- computed,
3
- makeObservable,
4
- observable,
5
- runInAction,
6
- action,
7
- toJS
8
- } from 'mobx'
9
-
10
- import { computedFn } from 'mobx-utils'
11
-
12
- import type {
13
- CommerceService,
14
- Family,
15
- LineItem,
16
- SelectedPaths,
17
- CategoryNode,
18
- CategoryNodeRole,
19
- Promo
20
- } from '../../../types'
21
-
22
- import {
23
- createOrder as createOrderHelper,
24
- updateOrderShippingInfo as updateOrderShippingInfoHelper,
25
- updateOrderPaymentInfo as updateOrderPaymentInfoHelper
26
- } from './orders'
27
-
28
- import ActualLineItem, { type ActualLineItemSnapshot } from './actual-line-item'
29
- import { getParentPath } from '../../../service/path-utils'
30
- import { getErrorMessage } from '../../../util'
31
- import sep from '../../sep'
32
-
33
- type StandaloneServiceOptions = {
34
- dbName: string
35
- ordersTable: string
36
- }
37
-
38
- interface StandaloneServiceSnapshot {
39
- items: ActualLineItemSnapshot[]
40
- }
41
-
42
-
43
- class StandaloneService
44
- implements CommerceService
45
- {
46
- private _familyMap = new Map<string, Family>()
47
- private _rootNode: CategoryNode
48
- private _selectedPaths: SelectedPaths = {}
49
- private _promo: Promo | null = null
50
-
51
- private _options : StandaloneServiceOptions
52
- private _currentFamily: Family | undefined = undefined
53
- private _currentItem: ActualLineItem | undefined = undefined
54
-
55
- constructor(
56
- families: Family[],
57
- rootNode: CategoryNode,
58
- options: StandaloneServiceOptions,
59
- serviceSnapshot?: StandaloneServiceSnapshot,
60
- ) {
61
-
62
- this._rootNode = rootNode
63
- this._options = options
64
-
65
- families.forEach((fam) => {
66
- fam.products = fam.products.map((p) => {
67
- if (serviceSnapshot) {
68
- const itemSnapshot = serviceSnapshot.items.find((is) => (is.sku === p.sku))
69
- if (itemSnapshot) {
70
- return new ActualLineItem(p, itemSnapshot)
71
- }
72
- }
73
- return new ActualLineItem(p)
74
- })
75
- this._familyMap.set(fam.id, fam)
76
- })
77
-
78
- makeObservable<
79
- StandaloneService,
80
- '_selectedPaths' |
81
- '_currentItem' |
82
- '_currentFamily' |
83
- '_promo'
84
- >(this, {
85
- _selectedPaths : observable.deep,
86
- _currentItem: observable.shallow,
87
- _currentFamily: observable.shallow,
88
- _promo: observable,
89
- })
90
-
91
- makeObservable(this, {
92
- cartItems: computed,
93
- cartQuantity: computed,
94
- cartTotal: computed,
95
- promoAppliedCartTotal: computed,
96
- cartEmpty: computed,
97
- selectedItems: computed,
98
- selectedFamilies: computed,
99
- hasSelection: computed,
100
- setCurrentItem: action,
101
- setCurrentFamily: action,
102
- currentItem: computed,
103
- currentFamily: computed,
104
- item: computed,
105
- family: computed,
106
- selectedPaths: computed,
107
- appliedPromo: computed,
108
- setAppliedPromo: action,
109
- /* NOT selectPaths. It implements it's action mechanism */
110
- })
111
- }
112
-
113
- getFamilyById(id: string): Family | undefined {
114
- return this._familyMap.get(id)
115
- }
116
-
117
- getNodeAtPath(skuPath: string): CategoryNode | undefined {
118
- const toks = skuPath.split(sep.tok)
119
- let level = 1
120
- let node: CategoryNode | undefined = this._rootNode
121
- do {
122
- node = node!.subNodes?.find((sn) => (sn.skuToken === toks[level]))
123
- level++
124
- }
125
- while (node && (level < toks.length))
126
- return level === toks.length ? node : undefined
127
- }
128
-
129
- peek(skuPath: string): {
130
- role: CategoryNodeRole
131
- family: Family | undefined
132
- families: Family[] | undefined
133
- node: CategoryNode | undefined
134
- item: LineItem | undefined
135
- } | string /* OR error string */ {
136
-
137
- const toks = skuPath.split(sep.tok)
138
- let level: number
139
- let node: CategoryNode | undefined = this._rootNode
140
- let parent: CategoryNode | undefined = undefined
141
-
142
- for (level = 1; level < toks.length && node && node.subNodes; level++) {
143
- // https://stackoverflow.com/questions/62367492/inference-problem-referenced-directly-or-indirectly-in-its-own-initializer
144
- const _node: CategoryNode | undefined =
145
- node!.subNodes.find((sn) => (sn.skuToken === toks[level]))
146
- if (!_node) {
147
- return `service.peekAtNode: traversing '${skuPath}'... no CategoryNode at '${toks[level]}'!`
148
- }
149
- parent = node
150
- node = _node
151
- }
152
-
153
- const atEnd = level === toks.length
154
- const possibleSKU = level === toks.length - 1
155
-
156
- let role: CategoryNodeRole = 'non-outermost'
157
- let families: Family[] | undefined = undefined
158
- let family: Family | undefined = undefined
159
- let item: LineItem | undefined = undefined
160
- let error: string | undefined = undefined
161
-
162
- try {
163
- if (node.subNodes && atEnd && node.outermost) {
164
- role = 'multi-family'
165
- families = node.subNodes.map((sub) => {
166
- const familyId = skuPath + sep.tok + sub.skuToken
167
- const fam = this._familyMap.get(familyId)
168
- if (!fam) {
169
- throw new Error(`service.peekAtNode: No Family under for CategoryNode '${skuPath}' with id ${familyId}!`)
170
- }
171
- return fam
172
- })
173
- }
174
- else if (!node.subNodes && (atEnd || possibleSKU)) {
175
- const _skuPath = (possibleSKU) ? getParentPath(skuPath) : skuPath
176
- if (parent?.outermost) {
177
- role = 'family-in-multi-family'
178
- const fam = this._familyMap.get(_skuPath)
179
- if (!fam) {
180
- throw new Error(`service.peekAtNode: '${_skuPath}' graphs as a Family under a multi-family node, but no such family exists!`)
181
- }
182
- family = fam
183
- const parentPath = getParentPath(_skuPath)
184
- // get all siblings (subnodes of parent)
185
- families = parent.subNodes!.map((sn) => {
186
- const familyId = parentPath + sep.tok + sn.skuToken
187
- const fam = this._familyMap.get(familyId)
188
- if (!fam) {
189
- throw new Error(`service.peekAtNode: No sibling Family for '${_skuPath}' with id '${familyId}'!`)
190
- }
191
- return fam
192
- })
193
- node = parent
194
- }
195
- else {
196
- role = 'single-family'
197
- const fam = this._familyMap.get(_skuPath)
198
- if (!fam) {
199
- throw new Error(`service.peekAtNode: '${_skuPath}' graphs as a single Family, but no such family exists!`)
200
- }
201
- family = fam
202
- }
203
- if (possibleSKU) {
204
- const skuToTry = family.id + sep.tok + toks[toks.length - 1]
205
- const _item = family.products.find((p) => (p.sku === skuToTry))
206
- if (_item) {
207
- item = _item as LineItem
208
- }
209
- else {
210
- throw new Error(`service.peekAtNode: '${skuPath}' graphs as LineItem in Family '${family.id}', but no such sku exists there!`)
211
- }
212
- }
213
- }
214
- }
215
- catch (e) {
216
- error = getErrorMessage(e)
217
- }
218
-
219
- return error ?? {
220
- role,
221
- family,
222
- families,
223
- node,
224
- item
225
- }
226
- }
227
-
228
- getSelectedNodesAtLevel = computedFn((level: number): CategoryNode[] | undefined => {
229
-
230
- let lvl = 1
231
- let nodesAtLevel: CategoryNode[] | undefined = this._rootNode.subNodes
232
-
233
- do {
234
- let selectedAtLevel: CategoryNode[] | undefined = undefined
235
- // If not specified, assume all
236
- if (lvl in this._selectedPaths) {
237
- selectedAtLevel = nodesAtLevel!.filter((n) => (this._selectedPaths[lvl].includes(n.skuToken)))
238
- }
239
- else {
240
- selectedAtLevel = nodesAtLevel
241
- }
242
- let allSubsOfSelected: CategoryNode[] = []
243
- selectedAtLevel?.forEach((n: CategoryNode) => {
244
- if (n.subNodes) {
245
- allSubsOfSelected = [...allSubsOfSelected, ...n.subNodes]
246
- }
247
- })
248
-
249
- nodesAtLevel = allSubsOfSelected
250
- lvl++
251
- } while (nodesAtLevel.length > 0 && lvl <= level)
252
-
253
- return (nodesAtLevel.length > 0 && ((lvl - 1) === level)) ? nodesAtLevel : undefined
254
- })
255
-
256
-
257
- //async createOrder(email: string, paymentMethod: string): Promise<string | undefined> {
258
- async createOrder(email: string, name?: string): Promise<string | undefined> {
259
- const snapshot = this.takeSnapshot()
260
- const order = await createOrderHelper(email, snapshot.items, this._options, name) // didn't want to have two levels of 'items'
261
- return order.id
262
- }
263
-
264
- // TODO: add shippingInfo type
265
- async updateOrderShippingInfo(orderId: string, shippingInfo: any): Promise<void> {
266
- updateOrderShippingInfoHelper(orderId, shippingInfo, this._options)
267
- }
268
-
269
- // TODO: add paymentInfo type
270
- async updateOrderPaymentInfo(orderId: string, paymentInfo: any): Promise<void> {
271
- updateOrderPaymentInfoHelper(orderId, paymentInfo, this._options)
272
- }
273
-
274
- takeSnapshot = (): StandaloneServiceSnapshot => ({
275
- items : (this.cartItems as ActualLineItem[]).map((it) => (it.takeSnapshot(this)))
276
- })
277
-
278
- get cartItems(): LineItem[] {
279
- let result: LineItem[] = []
280
- this._familyMap.forEach((fam) => {
281
- result = [...result, ...(fam.products as LineItem[]).filter((item) => (item.isInCart))]
282
- })
283
- return result.sort((it1, it2) => ((it1 as ActualLineItem).timeAdded - (it2 as ActualLineItem).timeAdded))
284
- }
285
-
286
- get cartEmpty(): boolean {
287
- return this.cartItems.length === 0
288
- }
289
-
290
- get cartTotal(): number {
291
- return this.cartItems.reduce(
292
- (total, item) => (total + item.price * item.quantity),
293
- 0
294
- )
295
- }
296
-
297
- _promoValue_unsafe(value: number): number {
298
- if (this._promo!.type === 'percent') {
299
- return value * (1 - this._promo!.value / 100)
300
- }
301
- return value - this._promo!.value
302
- }
303
-
304
- get promoAppliedCartTotal(): number {
305
- if (!this._promo) {
306
- return this.cartTotal
307
- }
308
- if (!this._promo.skus) {
309
- return this._promoValue_unsafe(this.cartTotal)
310
- }
311
- let total = this.cartItems.reduce(
312
- (total, item) => {
313
- const itemPrice = this._promo!.skus!.includes(item.sku) ?
314
- this._promoValue_unsafe(item.price)
315
- :
316
- item.price
317
- return total + itemPrice * item.quantity
318
- },
319
- 0
320
- )
321
- return total
322
- }
323
-
324
- itemPromoPrice(item: LineItem): number | undefined {
325
- if (this._promo && (!this._promo.skus || this._promo.skus.includes(item.sku) )) {
326
- return this._promoValue_unsafe(item.price)
327
- }
328
- return undefined
329
- }
330
-
331
- get cartQuantity(): number {
332
- return this.cartItems.reduce(
333
- (total, item) => (total + item.quantity),
334
- 0
335
- )
336
- }
337
-
338
- get appliedPromo(): Promo | null {
339
- return this._promo
340
- }
341
-
342
- setAppliedPromo(promo: Promo | null): void {
343
- this._promo = promo
344
- }
345
-
346
- getItemBySku = (skuToFind: string | undefined): LineItem | undefined => {
347
-
348
- if (skuToFind === undefined || skuToFind.length === 0) {
349
- return undefined
350
- }
351
- // Self-calling
352
- const found = ((): ActualLineItem | undefined => {
353
-
354
- const familiesTried: string[] = []
355
- if (this.selectedFamilies && this.selectedFamilies.length > 0) {
356
- for (let family of this.selectedFamilies) {
357
- familiesTried.push(family.id)
358
- const foundItem = family.products.find((p) => (p.sku === skuToFind))
359
- if (foundItem) {
360
- return foundItem as ActualLineItem
361
- }
362
- }
363
- }
364
- for( const [familyId, family] of this._familyMap.entries()) {
365
- if (familiesTried.includes(familyId)) continue
366
- const foundItem = family.products.find((p) => (p.sku === skuToFind)) as ActualLineItem | undefined
367
- if (foundItem) {
368
- return foundItem as ActualLineItem
369
- }
370
- }
371
- return undefined
372
- })(); // Self-calling, necessary semi
373
-
374
- return found
375
- }
376
-
377
- setCurrentItem = (skuToFind: string | undefined): boolean => {
378
-
379
- if (skuToFind === undefined || skuToFind.length === 0) {
380
- this._currentItem = undefined
381
- return true
382
- }
383
- // self calling function
384
- this._currentItem = this.getItemBySku(skuToFind) as ActualLineItem | undefined
385
- this.setCurrentFamily(this._currentItem ? this._currentItem.familyId : undefined)
386
- return !!this._currentItem
387
- }
388
-
389
- /* for ObsLineItemRef */
390
- get item(): LineItem | undefined {
391
- return this._currentItem
392
- }
393
-
394
- get currentItem(): LineItem | undefined {
395
- return this._currentItem
396
- }
397
-
398
- setCurrentFamily(id: string | undefined): boolean {
399
-
400
- if (id === undefined || id.length === 0) {
401
- this._currentFamily = undefined
402
- return true
403
- }
404
-
405
- const fam = this._familyMap.get(id)
406
- this._currentFamily = fam // undef ok
407
-
408
- if (
409
- this._currentFamily &&
410
- this._currentItem &&
411
- this._currentItem.familyId !== this._currentFamily.id
412
- ) {
413
- this._currentItem = undefined
414
- }
415
-
416
- return !!this._currentFamily
417
- }
418
-
419
- get currentFamily(): Family | undefined {
420
- return this._currentFamily
421
- }
422
-
423
- /* for ObsFamilyRef */
424
- get family(): Family | undefined {
425
- return this._currentFamily
426
- }
427
-
428
- selectPaths(sel: SelectedPaths): Family[] {
429
- runInAction (() => {
430
- this._selectedPaths = this._processAndValidate(sel)
431
- })
432
- return this.selectedFamilies
433
- }
434
-
435
- selectPath(skuPath: string): Family[] {
436
- const toks = skuPath.split(sep.tok)
437
- const highestLevel = toks.length - 1
438
- const fsv: SelectedPaths = {}
439
- for (let level = 1; level <= highestLevel; level++ ) {
440
- fsv[level] = [toks[level]]
441
- }
442
- return this.selectPaths(fsv)
443
- }
444
-
445
- get selectedPaths(): SelectedPaths {
446
- const result: SelectedPaths = {}
447
- for( let level in this._selectedPaths ) {
448
- result[level] = [...this._selectedPaths[level]]
449
- }
450
- return result
451
- }
452
-
453
- get selectedFamilies(): Family[] {
454
- if (Object.keys(toJS(this._selectedPaths)).length === 0) {
455
- // FacetsDesc have never been set or unset, so cannot evaluate them
456
- return []
457
- }
458
-
459
- return this._rootNode.subNodes!.reduce(
460
- (acc: Family[], subFacet: CategoryNode) => (
461
- // Pass the root token as a one member array
462
- this._reduceNode([this._rootNode.skuToken], acc, subFacet)
463
- ),
464
- []
465
- )
466
- }
467
-
468
- private _reduceNode(parentPath: string[], acc: Family[], node: CategoryNode): Family[] {
469
- const path = [...parentPath, node.skuToken] // Don't mutate original please :)
470
- const level = path.length - 1
471
- // If there is no token array supplied for this level,
472
- // assume all are specified. Otherwise, see if the
473
- // current node is in the array
474
- const specified = (
475
- !this._selectedPaths[level]
476
- ||
477
- this._selectedPaths[level].includes(node.skuToken)
478
- )
479
- if (specified) {
480
- // Process subnodes
481
- if (node.subNodes && node.subNodes.length > 0) {
482
- return node.subNodes.reduce((acc, n) => (
483
- this._reduceNode(path, acc, n)
484
- )
485
- , acc)
486
- }
487
- // Process leaf
488
- const fam = this._familyMap.get(path.join(sep.tok))
489
- if (!fam) {
490
- throw new Error("selectedFamilies WTF?!" + path.join(sep.tok))
491
- }
492
- acc.push(fam)
493
- }
494
- return acc
495
- }
496
-
497
- private _processAndValidate(partial: SelectedPaths): SelectedPaths {
498
-
499
- const result: SelectedPaths = {}
500
-
501
- let level = 1
502
- let currentSet = this._rootNode.subNodes!
503
-
504
- while (true) {
505
- let possibleCurrent = currentSet.map((el) => (el.skuToken))
506
- const validTokens = !partial[level] ? undefined : partial[level].filter((tok) => possibleCurrent.includes(tok))
507
- if (!validTokens) {
508
- break
509
- }
510
- result[level] = validTokens
511
- currentSet = validTokens.map((tok) => {
512
- const fd = currentSet.find((node) => ( node.skuToken === tok ))
513
- return (fd && fd.subNodes && fd.subNodes.length > 0) ? fd.subNodes : []
514
- }).flat()
515
- level++
516
- }
517
-
518
- return result
519
- }
520
-
521
- get selectedItems(): LineItem[] {
522
- if (Object.keys(toJS(this._selectedPaths)).length === 0) {
523
- // FacetsDesc have never been set or unset, so cannot evaluate them
524
- return []
525
- }
526
-
527
- return this.selectedFamilies.reduce(
528
- (allProducts, fam) => ([...allProducts, ...(fam.products as LineItem[])]), [] as LineItem[])
529
- }
530
-
531
- get hasSelection(): boolean {
532
- return this.selectedFamilies.length > 0
533
- }
534
-
535
- getFamilySubtotal(familyId: string): number {
536
- const c = this._familyMap.get(familyId)!
537
- return (c.products as LineItem[]).reduce(
538
- // avoid floating point bs around zero
539
- (total, item) => (item.quantity > 0 ? total + item.price * item.quantity : total),
540
- 0
541
- )
542
- }
543
-
544
- }
545
-
546
- export {
547
- type StandaloneServiceOptions,
548
- type StandaloneServiceSnapshot,
549
- StandaloneService as default
550
- }