@infrab4a/connect-angular 5.8.20 → 5.8.21-beta.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infrab4a/connect-angular",
3
- "version": "5.8.20",
3
+ "version": "5.8.21-beta.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org"
6
6
  },
@@ -55,7 +55,7 @@ describe('NewCategoryStructureAdapter', () => {
55
55
 
56
56
  const result = await adapter.buildProductFilterByCategory(category)
57
57
 
58
- expect(categoryRepository.get).toHaveBeenCalledOnceWith({ id: '12' })
58
+ expect(categoryRepository.get).toHaveBeenCalledWith({ id: '12' })
59
59
  expect(result).toEqual({ id: { operator: Where.IN, value: ['9'] } })
60
60
  })
61
61
  })
@@ -18,6 +18,16 @@ describe('CategorySearchStrategy', () => {
18
18
  count: 0,
19
19
  }
20
20
 
21
+ const daysAgo = (days: number): Date => new Date(Date.now() - days * 24 * 60 * 60 * 1000)
22
+
23
+ const buildProduct = (overrides: Partial<Product> & { id: string }): Product =>
24
+ ({
25
+ stock: { quantity: 10 },
26
+ shoppingCount: 0,
27
+ createdAt: daysAgo(60),
28
+ ...overrides,
29
+ }) as Product
30
+
21
31
  beforeEach(() => {
22
32
  findCatalog = jasmine.createSpy('findCatalog').and.resolveTo(emptyResult)
23
33
  buildProductFilterByCategory = jasmine
@@ -90,4 +100,115 @@ describe('CategorySearchStrategy', () => {
90
100
  expect(findCatalog.calls.mostRecent().args[0].orderBy).toEqual({ createdAt: 'desc' })
91
101
  expect(findCatalog.calls.mostRecent().args[0].limits).toEqual(limits)
92
102
  })
103
+
104
+ it('should keep wishlist most-relevant as best-sellers without pins/Novo block', async () => {
105
+ const category = { id: '10', isWishlist: true, products: ['1', '2'] } as Category
106
+
107
+ await strategy.search({ category, filters: {}, limits, sort: 'most-relevant' }, shop)
108
+
109
+ expect(findCatalog).toHaveBeenCalledTimes(1)
110
+ expect(findCatalog.calls.mostRecent().args[0].orderBy.shoppingCount).toBe('desc')
111
+ })
112
+
113
+ describe('most-relevant in-memory order', () => {
114
+ const setupMostRelevant = (products: Product[], pinIds: string[]) => {
115
+ const category = {
116
+ id: '20',
117
+ isWishlist: false,
118
+ getMostRelevantByShop: () => pinIds,
119
+ } as unknown as Category
120
+
121
+ findCatalog.and.callFake(async (params: any) => {
122
+ if (params.fields?.length === 1 && params.fields[0] === 'id') {
123
+ return { data: products.map((p) => ({ id: p.id })), count: products.length }
124
+ }
125
+
126
+ return { data: products, count: products.length }
127
+ })
128
+
129
+ return category
130
+ }
131
+
132
+ it('should keep in-stock pin first when most-relevant', async () => {
133
+ const products = [
134
+ buildProduct({ id: 'C', shoppingCount: 100, createdAt: daysAgo(60) }),
135
+ buildProduct({ id: 'P', shoppingCount: 1, createdAt: daysAgo(60) }),
136
+ ]
137
+ const category = setupMostRelevant(products, ['P'])
138
+
139
+ const result = await strategy.search({ category, filters: {}, limits, sort: 'most-relevant' }, shop)
140
+
141
+ expect(result.data.map((p) => p.id)).toEqual(['P', 'C'])
142
+ })
143
+
144
+ it('should place in-stock Novo after pins ordered by createdAt desc', async () => {
145
+ const products = [
146
+ buildProduct({ id: 'C', shoppingCount: 100, createdAt: daysAgo(60) }),
147
+ buildProduct({ id: 'B', shoppingCount: 5, createdAt: daysAgo(20) }),
148
+ buildProduct({ id: 'A', shoppingCount: 5, createdAt: daysAgo(5) }),
149
+ buildProduct({ id: 'P', shoppingCount: 1, createdAt: daysAgo(60) }),
150
+ ]
151
+ const category = setupMostRelevant(products, ['P'])
152
+
153
+ const result = await strategy.search({ category, filters: {}, limits, sort: 'most-relevant' }, shop)
154
+
155
+ expect(result.data.map((p) => p.id)).toEqual(['P', 'A', 'B', 'C'])
156
+ })
157
+
158
+ it('should not duplicate a pinned Novo', async () => {
159
+ const products = [
160
+ buildProduct({ id: 'C', shoppingCount: 50, createdAt: daysAgo(60) }),
161
+ buildProduct({ id: 'P', shoppingCount: 1, createdAt: daysAgo(3) }),
162
+ ]
163
+ const category = setupMostRelevant(products, ['P'])
164
+
165
+ const result = await strategy.search({ category, filters: {}, limits, sort: 'most-relevant' }, shop)
166
+
167
+ expect(result.data.map((p) => p.id)).toEqual(['P', 'C'])
168
+ expect(result.data.filter((p) => p.id === 'P').length).toBe(1)
169
+ })
170
+
171
+ it('should keep age 0 and 30 days in Novo block and exclude 31 days', async () => {
172
+ const products = [
173
+ buildProduct({ id: 'old', shoppingCount: 80, createdAt: daysAgo(31) }),
174
+ buildProduct({ id: 'edge', shoppingCount: 5, createdAt: daysAgo(30) }),
175
+ buildProduct({ id: 'fresh', shoppingCount: 5, createdAt: daysAgo(0) }),
176
+ ]
177
+ const category = setupMostRelevant(products, [])
178
+
179
+ const result = await strategy.search({ category, filters: {}, limits, sort: 'most-relevant' }, shop)
180
+
181
+ expect(result.data.map((p) => p.id)).toEqual(['fresh', 'edge', 'old'])
182
+ })
183
+
184
+ it('should not treat invalid or future createdAt as Novo', async () => {
185
+ const products = [
186
+ buildProduct({ id: 'best', shoppingCount: 90, createdAt: daysAgo(60) }),
187
+ buildProduct({ id: 'invalid', shoppingCount: 5, createdAt: 'not-a-date' as any }),
188
+ buildProduct({ id: 'future', shoppingCount: 5, createdAt: new Date(Date.now() + 86400000) }),
189
+ ]
190
+ const category = setupMostRelevant(products, [])
191
+
192
+ const result = await strategy.search({ category, filters: {}, limits, sort: 'most-relevant' }, shop)
193
+
194
+ expect(result.data.map((p) => p.id)).toEqual(['best', 'invalid', 'future'])
195
+ })
196
+
197
+ it('should not boost OOS Novo above in-stock bestsellers', async () => {
198
+ const products = [
199
+ buildProduct({ id: 'best', shoppingCount: 100, createdAt: daysAgo(60), stock: { quantity: 5 } as any }),
200
+ buildProduct({
201
+ id: 'oos-novo',
202
+ shoppingCount: 1,
203
+ createdAt: daysAgo(2),
204
+ stock: { quantity: 0 } as any,
205
+ }),
206
+ ]
207
+ const category = setupMostRelevant(products, [])
208
+
209
+ const result = await strategy.search({ category, filters: {}, limits, sort: 'most-relevant' }, shop)
210
+
211
+ expect(result.data.map((p) => p.id)).toEqual(['best', 'oos-novo'])
212
+ })
213
+ })
93
214
  })
@@ -164,9 +164,16 @@ export class CategorySearchStrategy implements CatalogSearchStrategy {
164
164
  const firstProducts = totalResult.data
165
165
  .filter((product: any) => mostRelevants.includes(product.id) && product.stock.quantity > 0)
166
166
  .sort((a: any, b: any) => mostRelevants.indexOf(a.id) - mostRelevants.indexOf(b.id))
167
- const lastProducts = totalResult.data
168
- .filter((product: any) => !mostRelevants.includes(product.id))
169
- .concat(mostRelevantWithoutStock)
167
+
168
+ const nonPinnedProducts = totalResult.data.filter((product: any) => !mostRelevants.includes(product.id))
169
+ const novoProducts = nonPinnedProducts
170
+ .filter((product: any) => product.stock?.quantity > 0 && this.isCreatedWithinLastDays(product.createdAt, 30))
171
+ .sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
172
+ const restProducts = nonPinnedProducts.filter(
173
+ (product: any) => !(product.stock?.quantity > 0 && this.isCreatedWithinLastDays(product.createdAt, 30)),
174
+ )
175
+
176
+ const lastProducts = novoProducts.concat(restProducts).concat(mostRelevantWithoutStock)
170
177
  const categoryMostRelevants = firstProducts.concat(lastProducts)
171
178
  const resultFinal = categoryMostRelevants.slice(limits.offset, limits.offset + limits.limit)
172
179
 
@@ -178,4 +185,23 @@ export class CategorySearchStrategy implements CatalogSearchStrategy {
178
185
  distinct: totalResult.distinct,
179
186
  }
180
187
  }
188
+
189
+ private isCreatedWithinLastDays(createdAt: string | Date | undefined, days: number): boolean {
190
+ if (!createdAt || days <= 0) {
191
+ return false
192
+ }
193
+
194
+ const createdDate = createdAt instanceof Date ? createdAt : new Date(createdAt)
195
+ if (Number.isNaN(createdDate.getTime())) {
196
+ return false
197
+ }
198
+
199
+ const diffMs = Date.now() - createdDate.getTime()
200
+ if (diffMs < 0) {
201
+ return false
202
+ }
203
+
204
+ const diffDays = diffMs / (1000 * 60 * 60 * 24)
205
+ return diffDays <= days
206
+ }
181
207
  }
package/src/test.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  // Karma entry required by tsconfig.spec.json. Specs in this package are Jasmine-only.
2
+ ;(globalThis as any).global = globalThis
2
3
  export {}