@isoftdata/svelte-ecommerce 1.0.0-beta.3 → 1.0.0-beta.5

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,4 +1,5 @@
1
1
  import { convertAndApplyTemplate, validateInventoryListingDetails } from './index.js';
2
+ import financialNumber from 'financial-number';
2
3
  import { klona } from 'klona';
3
4
  const newInventoryListingDetailTemplate = Object.freeze({
4
5
  active: false,
@@ -79,9 +80,7 @@ export const buildEbayListing = (buildEbayListingInput) => {
79
80
  return;
80
81
  }
81
82
  // Check if part type + category configuration is inactive
82
- if (shouldSkipListingDueToInactiveConfiguration(inventoryRow, inventoryTypeListingDefaults)) {
83
- return; // Part type configuration set to inactive, do not create listing row
84
- }
83
+ const { shouldSkip, reason } = shouldSkipListingDueToInactiveConfiguration(inventoryRow, inventoryTypeListingDefaults);
85
84
  // TODO: consider the possibility of an override flag on the listing details
86
85
  // Pull out the various levels of defaults, if they exist
87
86
  const globalDefaults = ecommercePartnerConfiguration.defaults?.global;
@@ -110,10 +109,10 @@ export const buildEbayListing = (buildEbayListingInput) => {
110
109
  }
111
110
  // Static
112
111
  listingDetails.ecommercePartnerId = ecommercePartnerId;
113
- listingDetails.partnerSpecificDetails.merchantLocationKey ??
112
+ listingDetails.partnerSpecificDetails.merchantLocationKey =
114
113
  listingDetails.partnerSpecificDetails.merchantLocationKey ??
115
- matchingStoreConfig?.merchantLocationKey ??
116
- null;
114
+ matchingStoreConfig?.merchantLocationKey ??
115
+ null;
117
116
  // Mapped
118
117
  listingDetails.ecommerceCategoryId = listingDetails.ecommerceCategoryId
119
118
  ? listingDetails.ecommerceCategoryId
@@ -124,13 +123,21 @@ export const buildEbayListing = (buildEbayListingInput) => {
124
123
  listingDetails.inventoryId = inventoryRow.inventoryId;
125
124
  listingDetails.storeId = listingDetails.storeId || inventoryRow.storeId;
126
125
  listingDetails.sku = listingDetails.sku || inventoryRow.tagNumber;
127
- listingDetails.price = listingDetails.price || inventoryRow.retailPrice || null;
126
+ //listingDetails.price = listingDetails.price || inventoryRow.retailPrice || null
128
127
  listingDetails.upc = listingDetails.upc ?? inventoryRow.upc ?? null;
129
128
  listingDetails.weight = listingDetails.weight ?? inventoryRow.weight ?? null;
130
129
  listingDetails.shippingHeight = listingDetails.shippingHeight ?? inventoryRow.shippingHeight ?? null;
131
130
  listingDetails.shippingLength = listingDetails.shippingLength ?? inventoryRow.shippingLength ?? null;
132
131
  listingDetails.shippingWidth = listingDetails.shippingWidth ?? inventoryRow.shippingWidth ?? null;
133
132
  listingDetails.inventoryDescription = listingDetails.inventoryDescription ?? inventoryRow.description ?? null;
133
+ // Calculate price adjustments, if appropriate
134
+ listingDetails.price = computeAdjustedPrice({
135
+ existingListingPrice: listingDetails.price,
136
+ partTypeDefaults,
137
+ storeDefaults,
138
+ globalDefaults,
139
+ inventoryRow
140
+ });
134
141
  // For this MVP, we'll default to the existing listing info first (this would include any edits on the part config page, then part type, store, finally global or nothing)
135
142
  // TODO: I suppose we could've made this easier by also having a defaults-shaped stringified json column on inventorylistingdetail
136
143
  listingDetails.ecommerceConditionId = getFirstValidValue(listingDetails.ecommerceConditionId, partTypeDefaults?.conditionId, storeDefaults?.conditionId, globalDefaults?.conditionId);
@@ -160,8 +167,16 @@ export const buildEbayListing = (buildEbayListingInput) => {
160
167
  }
161
168
  // With all known fields set, validate for errors
162
169
  const validationErrors = validateInventoryListingDetails(listingDetails);
163
- // Set active, status and message props depending on validation result
164
- if (validationErrors.length) {
170
+ if (shouldSkip && reason) { // Set active, status and message props if matching configuration set to inActive
171
+ listingDetails.listingStatus = 'cancelled';
172
+ const errObj = {
173
+ type: 'inactive',
174
+ messages: [reason]
175
+ };
176
+ listingDetails.message = [errObj];
177
+ listingDetails.active = false;
178
+ }
179
+ else if (validationErrors.length) { // Set active, status and message props depending on validation result
165
180
  listingDetails.listingStatus = 'error';
166
181
  const errObj = {
167
182
  type: 'validation',
@@ -180,6 +195,66 @@ export const buildEbayListing = (buildEbayListingInput) => {
180
195
  // TODO: In the old func there's an explicit reassignment for convertedListingDetails but shouldn't matter if we're cloning?
181
196
  return listingDetails;
182
197
  };
198
+ /**
199
+ * Maps PriceLevel string to the corresponding inventory price field
200
+ */
201
+ const getPriceFromInventory = (priceType, inventoryRow) => {
202
+ switch (priceType) {
203
+ case 'Retail':
204
+ return inventoryRow.retailPrice;
205
+ case 'Wholesale':
206
+ return inventoryRow.wholesalePrice;
207
+ case 'Cost':
208
+ return inventoryRow.cost;
209
+ case 'List':
210
+ return inventoryRow.listPrice;
211
+ default:
212
+ return inventoryRow.retailPrice;
213
+ }
214
+ };
215
+ /**
216
+ * Computes the adjusted price for a listing based on the following priority:
217
+ * 1. If existingListingPrice is set (not null/undefined/0), return it as-is (user has manually adjusted)
218
+ * 2. Otherwise, determine base price from defaultPriceType in priority order:
219
+ * - partTypeDefaults.defaultPriceType
220
+ * - storeDefaults.defaultPriceType
221
+ * - globalDefaults.defaultPriceType
222
+ * - Ultimate fallback: inventoryRow.retailPrice
223
+ * 3. Apply pricingModifier (with same priority fallback, default 100 = no change)
224
+ * 4. Formula: basePrice * (pricingModifier / 100)
225
+ */
226
+ export const computeAdjustedPrice = ({ existingListingPrice, partTypeDefaults, storeDefaults, globalDefaults, inventoryRow, }) => {
227
+ // Step 1: If existing listing price is set (not null/undefined/0), return it as-is
228
+ // User has manually adjusted the price, so don't apply any modifiers
229
+ if (existingListingPrice != null && existingListingPrice !== 0) {
230
+ return existingListingPrice;
231
+ }
232
+ // Step 2: Determine the base price to use from inventory
233
+ // Check for defaultPriceType in priority order
234
+ const priceType = getFirstValidValue(partTypeDefaults?.defaultPriceType, storeDefaults?.defaultPriceType, globalDefaults?.defaultPriceType);
235
+ // Get price from inventory based on the price type
236
+ let basePrice = getPriceFromInventory(priceType, inventoryRow);
237
+ // Ultimate fallback to retailPrice if basePrice is still null
238
+ if (basePrice == null) {
239
+ basePrice = inventoryRow.retailPrice;
240
+ }
241
+ // If we still don't have a price, return retailPrice as is
242
+ if (basePrice == null) {
243
+ return inventoryRow.retailPrice;
244
+ }
245
+ // Step 3: Determine the pricing modifier with priority fallback
246
+ const pricingModifier = partTypeDefaults?.pricingModifier ??
247
+ storeDefaults?.pricingModifier ??
248
+ globalDefaults?.pricingModifier ??
249
+ 100;
250
+ // Step 4: Calculate adjusted price
251
+ // Formula: basePrice * (pricingModifier / 100)
252
+ // Using financialNumber for precision
253
+ const adjustedPriceStr = financialNumber(basePrice.toString())
254
+ .times(financialNumber(pricingModifier.toString()).times('0.01'))
255
+ .toString(2);
256
+ return parseFloat(adjustedPriceStr);
257
+ };
183
258
  const getFirstValidArray = (...arrays) => {
184
259
  return arrays.find(arr => arr != null && arr.length > 0) ?? [];
185
260
  };
@@ -291,32 +366,53 @@ function findMatchingPartTypeConfig(inventoryRow, inventoryTypeListingDetails) {
291
366
  }
292
367
  return inventoryTypeListingDetails.find(config => config.categoryName === '');
293
368
  }
369
+ // TODO: this needs a unit test
294
370
  // Needed a separate method to check what I think are all of the ways we'd need to check
295
371
  // part type
296
372
  function shouldSkipListingDueToInactiveConfiguration(inventoryRow, inventoryTypeListingDetails) {
297
373
  // Early return if no configurations to check
298
374
  if (!inventoryTypeListingDetails || inventoryTypeListingDetails.length === 0) {
299
- return false; // No restrictions, continue processing
375
+ return { shouldSkip: false }; // No restrictions, continue processing
300
376
  }
301
377
  // Early return if inventory has no category
302
378
  if (!inventoryRow.category) {
303
- return true; // No category to match against, continue processing
379
+ return { shouldSkip: false }; // No category to match against, continue processing
304
380
  }
305
381
  // Scenario 1: Single config with no category (applies to all categories)
306
382
  if (inventoryTypeListingDetails.length === 1 && !inventoryTypeListingDetails[0].categoryId) {
307
- return !inventoryTypeListingDetails[0].active; // Skip if inactive
383
+ const isInactive = !inventoryTypeListingDetails[0].active;
384
+ return {
385
+ shouldSkip: isInactive,
386
+ reason: isInactive
387
+ ? 'Matching single inventory type configuration with no category is set to inactive'
388
+ : undefined,
389
+ };
308
390
  }
309
391
  // Scenario 2 & 3: Multiple configs or single config with category
310
392
  // Try to find exact category match first
311
393
  const categoryMatch = inventoryTypeListingDetails.find(config => config.categoryName === inventoryRow.category);
312
394
  if (categoryMatch) {
313
- return !categoryMatch.active; // Skip if category match is inactive
395
+ const isInactive = !categoryMatch.active;
396
+ return {
397
+ shouldSkip: isInactive,
398
+ reason: isInactive
399
+ ? `Inventory type configuration for category "${inventoryRow.category}" is set to inactive`
400
+ : undefined,
401
+ };
314
402
  }
403
+ // TODO: I'm the least certain on this one
404
+ // We've gotten this far, now check if there's an inventorytype with an empty categoryid and it's set to inactive
315
405
  // No exact category match - check if there's a "no category" fallback
316
406
  const noCategoryConfig = inventoryTypeListingDetails.find(config => !config.categoryId);
317
407
  if (noCategoryConfig) {
318
- return !noCategoryConfig.active; // Skip if fallback is inactive
408
+ const isInactive = !noCategoryConfig.active;
409
+ return {
410
+ shouldSkip: isInactive,
411
+ reason: isInactive
412
+ ? 'Override inventory type configuration (no category) is set to inactive'
413
+ : undefined,
414
+ };
319
415
  }
320
- // No matching configuration found, continue processing
321
- return false;
416
+ // No matching configuration found that is explicitly set to inactive, continue processing
417
+ return { shouldSkip: false };
322
418
  }
package/dist/utils.d.ts CHANGED
@@ -90,7 +90,8 @@ export type EcommerceSharedDefaults = {
90
90
  oemNumberMapping?: string;
91
91
  packageType?: string;
92
92
  paymentPolicies?: Array<string>;
93
- pricingModifier?: number;
93
+ pricingModifier: number;
94
+ defaultPriceType: string | null;
94
95
  returnPolicies?: Array<string>;
95
96
  shippingLengthUnit?: string;
96
97
  shippingWeightUnit?: string;
package/package.json CHANGED
@@ -1,17 +1,6 @@
1
1
  {
2
2
  "name": "@isoftdata/svelte-ecommerce",
3
- "version": "1.0.0-beta.3",
4
- "scripts": {
5
- "dev": "vite dev",
6
- "build": "vite build && npm run prepack",
7
- "preview": "vite preview",
8
- "prepare": "svelte-kit sync || echo ''",
9
- "prepack": "svelte-kit sync && svelte-package && publint",
10
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
11
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
12
- "lint": "eslint . && prettier --check .",
13
- "format": "prettier --write ."
14
- },
3
+ "version": "1.0.0-beta.5",
15
4
  "files": [
16
5
  "dist",
17
6
  "!dist/**/*.test.*",
@@ -46,6 +35,7 @@
46
35
  "@sveltejs/package": "^2.0.0",
47
36
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
48
37
  "@types/node": "^22",
38
+ "@types/to-title-case": "^1.0.2",
49
39
  "eslint": "^9.36.0",
50
40
  "eslint-config-prettier": "^10.1.8",
51
41
  "eslint-plugin-svelte": "^3.12.4",
@@ -56,6 +46,7 @@
56
46
  "publint": "^0.3.2",
57
47
  "svelte": "^5.0.0",
58
48
  "svelte-check": "^4.0.0",
49
+ "tsx": "^4.21.0",
59
50
  "typescript": "^5.0.0",
60
51
  "typescript-eslint": "^8.44.1",
61
52
  "vite": "^6.0.0"
@@ -70,6 +61,7 @@
70
61
  "@isoftdata/svelte-button": "^2.1.1",
71
62
  "@isoftdata/svelte-checkbox": "^2.5.0",
72
63
  "@isoftdata/svelte-currency-input": "^2.0.1",
64
+ "@isoftdata/svelte-fieldset": "^2.0.1",
73
65
  "@isoftdata/svelte-input": "^2.1.1",
74
66
  "@isoftdata/svelte-modal": "^2.0.11",
75
67
  "@isoftdata/svelte-select": "^2.0.4",
@@ -78,7 +70,20 @@
78
70
  "@isoftdata/svelte-user-prompt": "^1.1.0",
79
71
  "@isoftdata/utility-string": "^2.2.0",
80
72
  "@lukeed/uuid": "2.0.1",
73
+ "camelcase": "^9.0.0",
74
+ "financial-number": "^5.0.0",
81
75
  "klona": "^2.0.6",
82
- "svelte": "^5.23.2"
76
+ "svelte": "^5.23.2",
77
+ "to-title-case": "^1.0.0"
78
+ },
79
+ "scripts": {
80
+ "dev": "vite dev",
81
+ "build": "vite build && npm run prepack",
82
+ "preview": "vite preview",
83
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
84
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
85
+ "lint": "eslint . && prettier --check .",
86
+ "format": "prettier --write .",
87
+ "test": "node --import tsx --test src/lib/helpers/listing.test.ts"
83
88
  }
84
- }
89
+ }