@hanzo/commerce 7.2.0 → 7.3.1

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