@isoftdata/svelte-ecommerce 1.0.0-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/README.md +58 -0
- package/dist/EcommerceCategoryMapConfiguration.svelte +266 -0
- package/dist/EcommerceCategoryMapConfiguration.svelte.d.ts +14 -0
- package/dist/EcommerceConditionMapConfiguration.svelte +214 -0
- package/dist/EcommerceConditionMapConfiguration.svelte.d.ts +10 -0
- package/dist/EcommerceConfiguration.svelte +195 -0
- package/dist/EcommerceConfiguration.svelte.d.ts +22 -0
- package/dist/EcommerceDefaults.svelte +357 -0
- package/dist/EcommerceDefaults.svelte.d.ts +12 -0
- package/dist/EcommerceListingDetails.svelte +986 -0
- package/dist/EcommerceListingDetails.svelte.d.ts +18 -0
- package/dist/EcommercePartTypeConfig.svelte +305 -0
- package/dist/EcommercePartTypeConfig.svelte.d.ts +15 -0
- package/dist/EcommerceStoreConfiguration.svelte +263 -0
- package/dist/EcommerceStoreConfiguration.svelte.d.ts +13 -0
- package/dist/PolicyList.svelte +66 -0
- package/dist/PolicyList.svelte.d.ts +10 -0
- package/dist/data/ebay.d.ts +11 -0
- package/dist/data/ebay.js +31 -0
- package/dist/data/htp.d.ts +9 -0
- package/dist/data/htp.js +22 -0
- package/dist/data/index.d.ts +4 -0
- package/dist/data/index.js +6 -0
- package/dist/data/types.d.ts +4 -0
- package/dist/data/types.js +1 -0
- package/dist/helpers/index.d.ts +3 -0
- package/dist/helpers/index.js +3 -0
- package/dist/helpers/listing.d.ts +22 -0
- package/dist/helpers/listing.js +324 -0
- package/dist/helpers/template.d.ts +31 -0
- package/dist/helpers/template.js +131 -0
- package/dist/helpers/validation.d.ts +2 -0
- package/dist/helpers/validation.js +91 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/utils.d.ts +321 -0
- package/dist/utils.js +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PackageType } from './types.js';
|
|
2
|
+
export type LengthUnit = 'INCH' | 'FEET' | 'CENTIMETER' | 'METER';
|
|
3
|
+
export type WeightUnit = 'POUND' | 'KILOGRAM' | 'OUNCE' | 'GRAM';
|
|
4
|
+
export type TimeUnit = 'YEAR' | 'MONTH' | 'DAY' | 'HOUR' | 'CALENDAR_DAY' | 'BUSINESS_DAY';
|
|
5
|
+
export interface EbayStaticData {
|
|
6
|
+
lengthUnits: LengthUnit[];
|
|
7
|
+
weightUnits: WeightUnit[];
|
|
8
|
+
timeUnits: TimeUnit[];
|
|
9
|
+
packageTypes: PackageType[];
|
|
10
|
+
}
|
|
11
|
+
export declare const ebayStaticData: EbayStaticData;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const ebayStaticData = {
|
|
2
|
+
lengthUnits: ['INCH', 'FEET', 'CENTIMETER', 'METER'],
|
|
3
|
+
weightUnits: ['POUND', 'KILOGRAM', 'OUNCE', 'GRAM'],
|
|
4
|
+
timeUnits: ['YEAR', 'MONTH', 'DAY', 'HOUR', 'CALENDAR_DAY', 'BUSINESS_DAY'],
|
|
5
|
+
packageTypes: [
|
|
6
|
+
{
|
|
7
|
+
name: 'Bulky Goods',
|
|
8
|
+
partnerValue: 'BULKY_GOODS',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'Letter',
|
|
12
|
+
partnerValue: 'LETTER',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'Mailing Box',
|
|
16
|
+
partnerValue: 'MAILING_BOX',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'Pallet',
|
|
20
|
+
partnerValue: 'ONE_WAY_PALLET',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'Parcel or Padded Envelope',
|
|
24
|
+
partnerValue: 'PARCEL_OR_PADDED_ENVELOPE',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'USPS Flat Rate Envelope',
|
|
28
|
+
partnerValue: 'USPS_FLAT_RATE_ENVELOPE',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PackageType } from './types.js';
|
|
2
|
+
export type LengthUnit = 'in' | 'cm' | 'm' | 'ft';
|
|
3
|
+
export type WeightUnit = 'g' | 'lb' | 'kg' | 'oz';
|
|
4
|
+
export interface HtpStaticData {
|
|
5
|
+
lengthUnits: LengthUnit[];
|
|
6
|
+
weightUnits: WeightUnit[];
|
|
7
|
+
packageTypes: PackageType[];
|
|
8
|
+
}
|
|
9
|
+
export declare const htpStaticData: HtpStaticData;
|
package/dist/data/htp.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const htpStaticData = {
|
|
2
|
+
lengthUnits: ['in', 'cm', 'm', 'ft'],
|
|
3
|
+
weightUnits: ['g', 'lb', 'kg', 'oz'],
|
|
4
|
+
packageTypes: [
|
|
5
|
+
{
|
|
6
|
+
name: 'Box',
|
|
7
|
+
partnerValue: 'Box',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: 'Envelope',
|
|
11
|
+
partnerValue: 'Envelope',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'Letter',
|
|
15
|
+
partnerValue: 'Letter',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'Pallet',
|
|
19
|
+
partnerValue: 'Pallet',
|
|
20
|
+
},
|
|
21
|
+
]
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BuildEbayListingInput, FileItem, ImageUrl, InventoryListingDetail, NewInventoryListingDetail } from '../utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared method for creating an inventorylistingdetail row
|
|
4
|
+
* Includes minimum validation and may return undefined if certain configuration settings determine the inventory item doesn't get a listing created
|
|
5
|
+
*/
|
|
6
|
+
export declare const buildEbayListing: (buildEbayListingInput: BuildEbayListingInput) => InventoryListingDetail | NewInventoryListingDetail | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Accepts:
|
|
9
|
+
* the raw string of imageUrls from the listing, should be stringified json
|
|
10
|
+
* file list results from db
|
|
11
|
+
* Returns: stringified imageUrl list that can be tacked right onto the listing object for insert/update
|
|
12
|
+
*
|
|
13
|
+
* This function tries to compare the most recent file list for the inventory record with anything saved on the listing and:
|
|
14
|
+
* Filters for acceptable mime types
|
|
15
|
+
* Adds new items to the url array
|
|
16
|
+
* Updates existing items if rank or name changes
|
|
17
|
+
* Re-sorts to keep rank 1 at "top"
|
|
18
|
+
*
|
|
19
|
+
* TODO: need to make sure a specific type is used for file list so it's sharable
|
|
20
|
+
* TODO: I believe there's a max 12 image, probably have to account for that including returning a message if exceeded?
|
|
21
|
+
*/
|
|
22
|
+
export declare function processImageUrls(partFileList: FileItem[]): ImageUrl[] | null;
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { convertAndApplyTemplate, validateInventoryListingDetails } from './index.js';
|
|
2
|
+
import klona from 'klona';
|
|
3
|
+
const newInventoryListingDetailTemplate = {
|
|
4
|
+
active: false,
|
|
5
|
+
convertedListingDetails: null,
|
|
6
|
+
duration: 'GTC',
|
|
7
|
+
ecommercePartnerId: 0,
|
|
8
|
+
ecommerceCategoryId: null,
|
|
9
|
+
ecommerceConditionId: null,
|
|
10
|
+
ecommerceConditionDescription: null,
|
|
11
|
+
fulfillmentTime: null,
|
|
12
|
+
fulfillmentTimeUnit: null,
|
|
13
|
+
imageUrls: [],
|
|
14
|
+
inventoryId: 0,
|
|
15
|
+
inventoryDescription: null,
|
|
16
|
+
lastUpdate: '',
|
|
17
|
+
listingDescription: null,
|
|
18
|
+
listingStatus: 'pending',
|
|
19
|
+
listingTitle: null,
|
|
20
|
+
manufacturerPartNumber: null,
|
|
21
|
+
message: [],
|
|
22
|
+
partnerSpecificDetails: {
|
|
23
|
+
brand: '',
|
|
24
|
+
listingFormat: 'FIXED_PRICE',
|
|
25
|
+
fulfillmentPolicy: [],
|
|
26
|
+
marketplaceId: 'EBAY_MOTORS',
|
|
27
|
+
merchantLocationKey: '',
|
|
28
|
+
oemNumber: '',
|
|
29
|
+
paymentPolicy: [],
|
|
30
|
+
returnPolicy: []
|
|
31
|
+
},
|
|
32
|
+
price: null,
|
|
33
|
+
quantity: null,
|
|
34
|
+
shippingLength: null,
|
|
35
|
+
shippingWidth: null,
|
|
36
|
+
shippingHeight: null,
|
|
37
|
+
shippingLengthUnit: null,
|
|
38
|
+
shippingPackage: null,
|
|
39
|
+
sku: null,
|
|
40
|
+
storeId: 0,
|
|
41
|
+
upc: '',
|
|
42
|
+
weight: null,
|
|
43
|
+
weightUnit: null
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Shared method for creating an inventorylistingdetail row
|
|
47
|
+
* Includes minimum validation and may return undefined if certain configuration settings determine the inventory item doesn't get a listing created
|
|
48
|
+
*/
|
|
49
|
+
export const buildEbayListing = (buildEbayListingInput) => {
|
|
50
|
+
const { categoryMapping, conditionMapping, ecommercePartnerConfiguration, existingListing, inventoryRow, inventoryTypeListingDefaults, inventoryType, partFileList } = buildEbayListingInput;
|
|
51
|
+
if (!ecommercePartnerConfiguration) {
|
|
52
|
+
// No partner config, no listing
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const ecommercePartnerId = ecommercePartnerConfiguration.ecommercePartnerId;
|
|
56
|
+
// Check various fields/configs to determine if we should go skip creating the listing details
|
|
57
|
+
if (!inventoryRow.isWorldViewable) {
|
|
58
|
+
// Worldviewable is false, do not create listing row
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// TODO: this is problematic, we need to offer a way out of 'cancelled'
|
|
62
|
+
// Alternatively, build the listing anyway but keep the status?
|
|
63
|
+
if (existingListing && existingListing.listingStatus === 'cancelled') {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Is the ecommerce config set to active?
|
|
67
|
+
if (ecommercePartnerConfiguration && !ecommercePartnerConfiguration.active) {
|
|
68
|
+
// Ecommerce config set to inactive
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Is store mapped and active?
|
|
72
|
+
const matchingStoreConfig = ecommercePartnerConfiguration?.defaults?.store?.find(store => store.storeId === inventoryRow.storeId);
|
|
73
|
+
if (!matchingStoreConfig) {
|
|
74
|
+
// No store/location configured, do not create listing row
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (matchingStoreConfig && !matchingStoreConfig.active) {
|
|
78
|
+
// Store configured to inactive, do not create listing row
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// 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
|
+
}
|
|
85
|
+
// TODO: consider the possibility of an override flag on the listing details
|
|
86
|
+
// Pull out the various levels of defaults, if they exist
|
|
87
|
+
const globalDefaults = ecommercePartnerConfiguration.defaults?.global;
|
|
88
|
+
const storeDefaults = ecommercePartnerConfiguration.defaults?.store?.find(row => row?.storeId === inventoryRow.storeId)?.defaults;
|
|
89
|
+
const partTypeDefaults = findMatchingPartTypeConfig(inventoryRow, inventoryTypeListingDefaults)?.defaults;
|
|
90
|
+
// Clone existing listing details or template to start
|
|
91
|
+
let listingDetails;
|
|
92
|
+
if (existingListing) {
|
|
93
|
+
listingDetails = klona(existingListing);
|
|
94
|
+
// Also, if partnerSpecificDetails doesn't exist, create it. (Get weird type errors if we don't)
|
|
95
|
+
if (!listingDetails.partnerSpecificDetails || Object.keys(listingDetails.partnerSpecificDetails).length === 0) {
|
|
96
|
+
listingDetails.partnerSpecificDetails = {
|
|
97
|
+
brand: '',
|
|
98
|
+
listingFormat: 'FIXED_PRICE',
|
|
99
|
+
fulfillmentPolicy: [],
|
|
100
|
+
marketplaceId: 'EBAY_MOTORS',
|
|
101
|
+
merchantLocationKey: '',
|
|
102
|
+
oemNumber: '',
|
|
103
|
+
paymentPolicy: [],
|
|
104
|
+
returnPolicy: []
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
listingDetails = klona(newInventoryListingDetailTemplate);
|
|
110
|
+
}
|
|
111
|
+
// Static
|
|
112
|
+
listingDetails.ecommercePartnerId = ecommercePartnerId;
|
|
113
|
+
listingDetails.partnerSpecificDetails.merchantLocationKey ?? listingDetails.partnerSpecificDetails.merchantLocationKey ?? matchingStoreConfig?.merchantLocationKey ?? null;
|
|
114
|
+
// Mapped
|
|
115
|
+
listingDetails.ecommerceCategoryId = listingDetails.ecommerceCategoryId
|
|
116
|
+
? listingDetails.ecommerceCategoryId
|
|
117
|
+
: (categoryMapping?.ebayCategoryId ?? null);
|
|
118
|
+
// Use latest from inventory
|
|
119
|
+
// TODO: I'll need to check some of these, they may not allow changes on the config screen and inventory.* is the only truth avail
|
|
120
|
+
listingDetails.quantity = inventoryRow.quantity;
|
|
121
|
+
listingDetails.storeId = listingDetails.storeId || inventoryRow.storeId;
|
|
122
|
+
listingDetails.sku = listingDetails.sku || inventoryRow.tagNumber;
|
|
123
|
+
listingDetails.price = listingDetails.price || inventoryRow.retailPrice || null;
|
|
124
|
+
listingDetails.upc = listingDetails.upc ?? inventoryRow.upc ?? null;
|
|
125
|
+
listingDetails.weight = listingDetails.weight ?? inventoryRow.weight ?? null;
|
|
126
|
+
listingDetails.shippingHeight = listingDetails.shippingHeight ?? inventoryRow.shippingHeight ?? null;
|
|
127
|
+
listingDetails.shippingLength = listingDetails.shippingLength ?? inventoryRow.shippingLength ?? null;
|
|
128
|
+
listingDetails.shippingWidth = listingDetails.shippingWidth ?? inventoryRow.shippingWidth ?? null;
|
|
129
|
+
listingDetails.inventoryDescription = listingDetails.inventoryDescription ?? inventoryRow.description ?? null;
|
|
130
|
+
// 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)
|
|
131
|
+
// TODO: I suppose we could've made this easier by also having a defaults-shaped stringified json column on inventorylistingdetail
|
|
132
|
+
listingDetails.ecommerceConditionId = getFirstValidValue(listingDetails.ecommerceConditionId, partTypeDefaults?.conditionId, storeDefaults?.conditionId, globalDefaults?.conditionId);
|
|
133
|
+
listingDetails.ecommerceConditionDescription = getFirstValidValue(listingDetails.ecommerceConditionDescription, partTypeDefaults?.conditionDescription, storeDefaults?.conditionDescription, globalDefaults?.conditionDescription);
|
|
134
|
+
listingDetails.fulfillmentTime = getFirstValidValue(listingDetails.fulfillmentTime, partTypeDefaults?.fulfillmentTimeValue, storeDefaults?.fulfillmentTimeValue, globalDefaults?.fulfillmentTimeValue);
|
|
135
|
+
listingDetails.fulfillmentTimeUnit = getFirstValidValue(listingDetails.fulfillmentTimeUnit, partTypeDefaults?.fulfillmentTimeUnit, storeDefaults?.fulfillmentTimeUnit, globalDefaults?.fulfillmentTimeUnit);
|
|
136
|
+
listingDetails.partnerSpecificDetails.fulfillmentPolicy = getFirstValidArray(listingDetails.partnerSpecificDetails.fulfillmentPolicy, partTypeDefaults?.fulfillmentPolicies, storeDefaults?.fulfillmentPolicies, globalDefaults?.fulfillmentPolicies);
|
|
137
|
+
listingDetails.partnerSpecificDetails.paymentPolicy = getFirstValidArray(listingDetails.partnerSpecificDetails.paymentPolicy, partTypeDefaults?.paymentPolicies, storeDefaults?.paymentPolicies, globalDefaults?.paymentPolicies);
|
|
138
|
+
listingDetails.partnerSpecificDetails.returnPolicy = getFirstValidArray(listingDetails.partnerSpecificDetails.returnPolicy, partTypeDefaults?.returnPolicies, storeDefaults?.returnPolicies, globalDefaults?.returnPolicies);
|
|
139
|
+
listingDetails.shippingPackage = getFirstValidValue(listingDetails.shippingPackage, partTypeDefaults?.packageType, storeDefaults?.packageType, globalDefaults?.packageType);
|
|
140
|
+
listingDetails.weightUnit = getFirstValidValue(listingDetails.weightUnit, partTypeDefaults?.shippingWeightUnit, storeDefaults?.shippingWeightUnit, globalDefaults?.shippingWeightUnit);
|
|
141
|
+
listingDetails.shippingLengthUnit = getFirstValidValue(listingDetails.shippingLengthUnit, partTypeDefaults?.shippingLengthUnit, storeDefaults?.shippingLengthUnit, globalDefaults?.shippingLengthUnit);
|
|
142
|
+
// Handle Templates with same fallback pattern as above
|
|
143
|
+
listingDetails.partnerSpecificDetails.brand = getFirstValidTemplateValue(listingDetails.partnerSpecificDetails.brand, partTypeDefaults?.brandMapping, storeDefaults?.brandMapping, globalDefaults?.brandMapping, inventoryRow, inventoryType?.name);
|
|
144
|
+
listingDetails.listingTitle = getFirstValidTemplateValue(listingDetails.listingTitle, partTypeDefaults?.listingTitleTemplate, storeDefaults?.listingTitleTemplate, globalDefaults?.listingTitleTemplate, inventoryRow, inventoryType?.name);
|
|
145
|
+
listingDetails.listingDescription = getFirstValidTemplateValue(listingDetails.listingDescription, partTypeDefaults?.listingDescriptionTemplate, storeDefaults?.listingDescriptionTemplate, globalDefaults?.listingDescriptionTemplate, inventoryRow, inventoryType?.name);
|
|
146
|
+
listingDetails.manufacturerPartNumber = getFirstValidTemplateValue(listingDetails.manufacturerPartNumber, partTypeDefaults?.manufacturerPartNumberMapping, storeDefaults?.manufacturerPartNumberMapping, globalDefaults?.manufacturerPartNumberMapping, inventoryRow, inventoryType?.name);
|
|
147
|
+
listingDetails.partnerSpecificDetails.oemNumber = getFirstValidTemplateValue(listingDetails.partnerSpecificDetails.oemNumber, partTypeDefaults?.oemNumberMapping, storeDefaults?.oemNumberMapping, globalDefaults?.oemNumberMapping, inventoryRow, inventoryType?.name);
|
|
148
|
+
// Process image URLs if partFileList is provided
|
|
149
|
+
if (partFileList && partFileList.length > 0) {
|
|
150
|
+
const processedImageUrls = processImageUrls(partFileList);
|
|
151
|
+
listingDetails.imageUrls = processedImageUrls || [];
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Ensure imageUrls is always an array
|
|
155
|
+
listingDetails.imageUrls = listingDetails.imageUrls || [];
|
|
156
|
+
}
|
|
157
|
+
// With all known fields set, validate for errors
|
|
158
|
+
const validationErrors = validateInventoryListingDetails(listingDetails);
|
|
159
|
+
// Set active, status and message props depending on validation result
|
|
160
|
+
if (validationErrors.length) {
|
|
161
|
+
listingDetails.listingStatus = 'error';
|
|
162
|
+
const errObj = {
|
|
163
|
+
type: "validation",
|
|
164
|
+
messages: validationErrors
|
|
165
|
+
};
|
|
166
|
+
listingDetails.message = [errObj];
|
|
167
|
+
listingDetails.active = false;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Clean up old messages
|
|
171
|
+
listingDetails.message = [];
|
|
172
|
+
// set to pending
|
|
173
|
+
listingDetails.active = true;
|
|
174
|
+
listingDetails.listingStatus = 'pending';
|
|
175
|
+
}
|
|
176
|
+
// TODO: In the old func there's an explicit reassignment for convertedListingDetails but shouldn't matter if we're cloning?
|
|
177
|
+
return listingDetails;
|
|
178
|
+
};
|
|
179
|
+
const getFirstValidArray = (...arrays) => {
|
|
180
|
+
return arrays.find(arr => arr != null && arr.length > 0) ?? [];
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Returns the first non-null/undefined value from the provided arguments
|
|
184
|
+
*/
|
|
185
|
+
const getFirstValidValue = (...values) => {
|
|
186
|
+
return values.find(value => value != null) ?? null;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Returns the first valid template result using the fallback pattern:
|
|
190
|
+
* 1. Use existing value if it exists and is not empty
|
|
191
|
+
* 2. Try templates in order: partType -> store -> global
|
|
192
|
+
* 3. Return null if no valid template produces a result
|
|
193
|
+
*/
|
|
194
|
+
const getFirstValidTemplateValue = (existingValue, partTypeTemplate, storeTemplate, globalTemplate, inventoryRow, inventoryTypeName) => {
|
|
195
|
+
// If existing value exists and is not empty, use it
|
|
196
|
+
if (existingValue && existingValue.trim() !== '') {
|
|
197
|
+
return existingValue;
|
|
198
|
+
}
|
|
199
|
+
// Try templates in fallback order
|
|
200
|
+
const templates = [partTypeTemplate, storeTemplate, globalTemplate];
|
|
201
|
+
for (const template of templates) {
|
|
202
|
+
if (template) {
|
|
203
|
+
try {
|
|
204
|
+
const result = convertAndApplyTemplate({
|
|
205
|
+
inventory: inventoryRow,
|
|
206
|
+
inventoryTypeName,
|
|
207
|
+
template
|
|
208
|
+
});
|
|
209
|
+
if (result && result.trim() !== '') {
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
// Log error but continue to next template
|
|
215
|
+
console.warn('Template processing error:', error);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Accepts:
|
|
224
|
+
* the raw string of imageUrls from the listing, should be stringified json
|
|
225
|
+
* file list results from db
|
|
226
|
+
* Returns: stringified imageUrl list that can be tacked right onto the listing object for insert/update
|
|
227
|
+
*
|
|
228
|
+
* This function tries to compare the most recent file list for the inventory record with anything saved on the listing and:
|
|
229
|
+
* Filters for acceptable mime types
|
|
230
|
+
* Adds new items to the url array
|
|
231
|
+
* Updates existing items if rank or name changes
|
|
232
|
+
* Re-sorts to keep rank 1 at "top"
|
|
233
|
+
*
|
|
234
|
+
* TODO: need to make sure a specific type is used for file list so it's sharable
|
|
235
|
+
* TODO: I believe there's a max 12 image, probably have to account for that including returning a message if exceeded?
|
|
236
|
+
*/
|
|
237
|
+
export function processImageUrls(partFileList) {
|
|
238
|
+
// Accepted image MIME types for eBay picture service
|
|
239
|
+
const acceptedMimeTypes = [
|
|
240
|
+
'image/avif',
|
|
241
|
+
'image/bmp',
|
|
242
|
+
'image/gif',
|
|
243
|
+
'image/heic',
|
|
244
|
+
'image/jpeg',
|
|
245
|
+
'image/jpg',
|
|
246
|
+
'image/png',
|
|
247
|
+
'image/tiff',
|
|
248
|
+
'image/tif',
|
|
249
|
+
'image/webp'
|
|
250
|
+
];
|
|
251
|
+
// Create a new array that will only contain valid files
|
|
252
|
+
const updatedImageUrlArray = [];
|
|
253
|
+
// Process each file in the most recent partFileList results for an inventory item
|
|
254
|
+
for (const file of partFileList) {
|
|
255
|
+
// Check if the file has an accepted mimeType
|
|
256
|
+
if (!acceptedMimeTypes.includes(file.mimeType.toLowerCase())) {
|
|
257
|
+
continue; // Skip files with unsupported mimeTypes
|
|
258
|
+
}
|
|
259
|
+
// Check if the file is public (skip if public is false)
|
|
260
|
+
if (file.public === false) {
|
|
261
|
+
continue; // Skip files that are not public (removes them from existing list)
|
|
262
|
+
}
|
|
263
|
+
updatedImageUrlArray.push({
|
|
264
|
+
fileId: file.fileId,
|
|
265
|
+
fileName: file.name,
|
|
266
|
+
rank: file.rank ?? 0,
|
|
267
|
+
mimeType: file.mimeType
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// Sort by rank to maintain order, presumably the "top" image in the list of images given to eBay for a listing is shown first
|
|
271
|
+
updatedImageUrlArray.sort((a, b) => a.rank - b.rank);
|
|
272
|
+
// Stringify it so we return it in same format it was received
|
|
273
|
+
return updatedImageUrlArray.length > 0 ? updatedImageUrlArray : null;
|
|
274
|
+
}
|
|
275
|
+
// TODO: pretty much identical to the next method, could probably have just combined and returned two things
|
|
276
|
+
function findMatchingPartTypeConfig(inventoryRow, inventoryTypeListingDetails) {
|
|
277
|
+
// Early return if no configurations to check
|
|
278
|
+
if (!inventoryTypeListingDetails || inventoryTypeListingDetails.length === 0) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// Scenario 1: Single config with no category (applies to all categories)
|
|
282
|
+
if (inventoryTypeListingDetails.length === 1 && !inventoryTypeListingDetails[0].categoryId) {
|
|
283
|
+
return inventoryTypeListingDetails[0]; // Skip if inactive
|
|
284
|
+
}
|
|
285
|
+
const categoryMatch = inventoryTypeListingDetails.find(config => config.categoryName === inventoryRow.category);
|
|
286
|
+
if (categoryMatch) {
|
|
287
|
+
return categoryMatch;
|
|
288
|
+
}
|
|
289
|
+
// TODO: this one's iffy, double-check if types match reality, also make sure we can't have more than one
|
|
290
|
+
const allCategoryMatch = inventoryTypeListingDetails.find(config => config.categoryName === '');
|
|
291
|
+
if (allCategoryMatch) {
|
|
292
|
+
return allCategoryMatch;
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// Needed a separate method to check what I think are all of the ways we'd need to check
|
|
297
|
+
// part type
|
|
298
|
+
function shouldSkipListingDueToInactiveConfiguration(inventoryRow, inventoryTypeListingDetails) {
|
|
299
|
+
// Early return if no configurations to check
|
|
300
|
+
if (!inventoryTypeListingDetails || inventoryTypeListingDetails.length === 0) {
|
|
301
|
+
return false; // No restrictions, continue processing
|
|
302
|
+
}
|
|
303
|
+
// Early return if inventory has no category
|
|
304
|
+
if (!inventoryRow.category) {
|
|
305
|
+
return true; // No category to match against, continue processing
|
|
306
|
+
}
|
|
307
|
+
// Scenario 1: Single config with no category (applies to all categories)
|
|
308
|
+
if (inventoryTypeListingDetails.length === 1 && !inventoryTypeListingDetails[0].categoryId) {
|
|
309
|
+
return !inventoryTypeListingDetails[0].active; // Skip if inactive
|
|
310
|
+
}
|
|
311
|
+
// Scenario 2 & 3: Multiple configs or single config with category
|
|
312
|
+
// Try to find exact category match first
|
|
313
|
+
const categoryMatch = inventoryTypeListingDetails.find(config => config.categoryName === inventoryRow.category);
|
|
314
|
+
if (categoryMatch) {
|
|
315
|
+
return !categoryMatch.active; // Skip if category match is inactive
|
|
316
|
+
}
|
|
317
|
+
// No exact category match - check if there's a "no category" fallback
|
|
318
|
+
const noCategoryConfig = inventoryTypeListingDetails.find(config => !config.categoryId);
|
|
319
|
+
if (noCategoryConfig) {
|
|
320
|
+
return !noCategoryConfig.active; // Skip if fallback is inactive
|
|
321
|
+
}
|
|
322
|
+
// No matching configuration found, continue processing
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { InventoryRow, InventoryDetailsForTemplate } from "../utils.js";
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @param template Configured in ecommerce settings
|
|
5
|
+
* @returns a string with template values replaced by inventory values
|
|
6
|
+
* Convert product inventory row to a shared type for the token replacement
|
|
7
|
+
*/
|
|
8
|
+
export declare function convertAndApplyTemplate({ inventory, inventoryTypeName, template }: {
|
|
9
|
+
inventory: InventoryRow;
|
|
10
|
+
inventoryTypeName: string | null | undefined;
|
|
11
|
+
template: string | null | undefined;
|
|
12
|
+
}): string | null;
|
|
13
|
+
/**
|
|
14
|
+
* Replaces tokens in a template string with values from an inventory record
|
|
15
|
+
* @param template - The template string containing tokens like "{$PartType}"
|
|
16
|
+
* @param inventoryDetails - The inventory object containing values to replace tokens
|
|
17
|
+
* @returns The processed string with tokens replaced by values
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Example usage:
|
|
21
|
+
* const template = "This is a {$PartType} made by {$Manufacturer}, model {$AssemblyModel}"
|
|
22
|
+
* const inventoryDetails = {
|
|
23
|
+
* partType: "Engine",
|
|
24
|
+
* manufacturer: "Toyota",
|
|
25
|
+
* model: "2JZ-GTE"
|
|
26
|
+
* }
|
|
27
|
+
* const description = replaceInventoryTokens(template, inventoryDetails)
|
|
28
|
+
* // Result: "This is a Engine made by Toyota, model 2JZ-GTE"
|
|
29
|
+
* https://wikido.isoftdata.com//index.php?title=Ebay_templates
|
|
30
|
+
*/
|
|
31
|
+
export declare function replaceInventoryTokens(template: string, inventoryDetails: Partial<InventoryDetailsForTemplate>): string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param template Configured in ecommerce settings
|
|
4
|
+
* @returns a string with template values replaced by inventory values
|
|
5
|
+
* Convert product inventory row to a shared type for the token replacement
|
|
6
|
+
*/
|
|
7
|
+
export function convertAndApplyTemplate({ inventory, inventoryTypeName, template }) {
|
|
8
|
+
if (!template)
|
|
9
|
+
return null;
|
|
10
|
+
// Map the inventory object to the InventoryDetails type expected by replaceInventoryTokens
|
|
11
|
+
const mappedInventoryDetails = {
|
|
12
|
+
inventoryId: inventory.inventoryId,
|
|
13
|
+
vehicleMake: inventory.make || undefined,
|
|
14
|
+
vehicleModel: inventory.model || undefined,
|
|
15
|
+
year: inventory.year || undefined,
|
|
16
|
+
description: inventory.description || undefined,
|
|
17
|
+
price: inventory.retailPrice || undefined,
|
|
18
|
+
manufacturer: inventory.partManufacturer || undefined,
|
|
19
|
+
model: inventory.partModel || undefined,
|
|
20
|
+
partType: inventoryTypeName || undefined,
|
|
21
|
+
category: inventory.category || undefined,
|
|
22
|
+
store: inventory.storeId?.toString() || undefined,
|
|
23
|
+
tagNumber: inventory.tagNumber || undefined,
|
|
24
|
+
condition: inventory.condition || undefined,
|
|
25
|
+
flexLabel1: inventory.label1 || undefined,
|
|
26
|
+
flexLabel2: inventory.label2 || undefined,
|
|
27
|
+
flexLabel3: inventory.label3 || undefined,
|
|
28
|
+
flexLabel4: inventory.label4 || undefined,
|
|
29
|
+
flexValue1: inventory.data1 || undefined,
|
|
30
|
+
flexValue2: inventory.data2 || undefined,
|
|
31
|
+
flexValue3: inventory.data3 || undefined,
|
|
32
|
+
flexValue4: inventory.data4 || undefined,
|
|
33
|
+
oemNumber: inventory.oemNumber || undefined,
|
|
34
|
+
side: inventory.side || undefined,
|
|
35
|
+
serialNumber: inventory.serialNumber || undefined,
|
|
36
|
+
weight: inventory.weight || undefined,
|
|
37
|
+
upc: inventory.upc || undefined,
|
|
38
|
+
notes: inventory.notes || undefined,
|
|
39
|
+
quantity: inventory.quantity || undefined,
|
|
40
|
+
partManufacturer: inventory.partManufacturer || undefined,
|
|
41
|
+
partModel: inventory.partModel || undefined,
|
|
42
|
+
};
|
|
43
|
+
// Replace tokens in the template and set the listingDescription
|
|
44
|
+
return replaceInventoryTokens(template, mappedInventoryDetails);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Replaces tokens in a template string with values from an inventory record
|
|
48
|
+
* @param template - The template string containing tokens like "{$PartType}"
|
|
49
|
+
* @param inventoryDetails - The inventory object containing values to replace tokens
|
|
50
|
+
* @returns The processed string with tokens replaced by values
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Example usage:
|
|
54
|
+
* const template = "This is a {$PartType} made by {$Manufacturer}, model {$AssemblyModel}"
|
|
55
|
+
* const inventoryDetails = {
|
|
56
|
+
* partType: "Engine",
|
|
57
|
+
* manufacturer: "Toyota",
|
|
58
|
+
* model: "2JZ-GTE"
|
|
59
|
+
* }
|
|
60
|
+
* const description = replaceInventoryTokens(template, inventoryDetails)
|
|
61
|
+
* // Result: "This is a Engine made by Toyota, model 2JZ-GTE"
|
|
62
|
+
* https://wikido.isoftdata.com//index.php?title=Ebay_templates
|
|
63
|
+
*/
|
|
64
|
+
export function replaceInventoryTokens(template, inventoryDetails) {
|
|
65
|
+
if (!template)
|
|
66
|
+
return '';
|
|
67
|
+
if (!inventoryDetails)
|
|
68
|
+
return template;
|
|
69
|
+
// Define token mappings (token name -> inventory property)
|
|
70
|
+
const generalTokens = {
|
|
71
|
+
'{?InventoryID}': 'inventoryId',
|
|
72
|
+
'{?Make}': 'vehicleMake',
|
|
73
|
+
'{?Model}': 'model',
|
|
74
|
+
'{?Year}': 'year',
|
|
75
|
+
'{?Description}': 'description',
|
|
76
|
+
'{?Price}': 'price',
|
|
77
|
+
'{?Manufacturer}': 'manufacturer',
|
|
78
|
+
'{?AssemblyModel}': 'model',
|
|
79
|
+
'{?PartType}': 'partType',
|
|
80
|
+
'{?Category}': 'category',
|
|
81
|
+
'{?Store}': 'store',
|
|
82
|
+
'{?ProductCode}': 'productCode',
|
|
83
|
+
'{?TagNumber}': 'tagNumber',
|
|
84
|
+
'{?Condition}': 'condition',
|
|
85
|
+
'{?FlexLabel1}': 'flexLabel1',
|
|
86
|
+
'{?FlexLabel2}': 'flexLabel2',
|
|
87
|
+
'{?FlexLabel3}': 'flexLabel3',
|
|
88
|
+
'{?FlexLabel4}': 'flexLabel4',
|
|
89
|
+
'{?FlexValue1}': 'flexValue1',
|
|
90
|
+
'{?FlexValue2}': 'flexValue2',
|
|
91
|
+
'{?FlexValue3}': 'flexValue3',
|
|
92
|
+
'{?FlexValue4}': 'flexValue4',
|
|
93
|
+
'{?OEMNumber}': 'oemNumber',
|
|
94
|
+
'{?Side}': 'side'
|
|
95
|
+
// Add more mappings as needed
|
|
96
|
+
};
|
|
97
|
+
// Pro-specific token mappings
|
|
98
|
+
const proTokens = {
|
|
99
|
+
'{?PartNum}': 'inventoryId'
|
|
100
|
+
};
|
|
101
|
+
// Enterprise-specific token mappings, need to confirm these
|
|
102
|
+
const enterpriseTokenMappings = {
|
|
103
|
+
'{?PartNumber}': 'inventoryId', // not sure on this one, supposed to be vendor part number
|
|
104
|
+
'{?PartManufacturer}': 'partManufacturer',
|
|
105
|
+
'{?PartModel}': 'partModel'
|
|
106
|
+
};
|
|
107
|
+
// Combine all token mappings
|
|
108
|
+
const tokenMappings = {
|
|
109
|
+
...generalTokens,
|
|
110
|
+
...proTokens,
|
|
111
|
+
...enterpriseTokenMappings
|
|
112
|
+
};
|
|
113
|
+
// Replace each token with its corresponding value
|
|
114
|
+
let result = template;
|
|
115
|
+
for (const [token, property] of Object.entries(tokenMappings)) {
|
|
116
|
+
// Create a global regex pattern to replace all occurrences
|
|
117
|
+
const tokenPattern = new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
|
|
118
|
+
// Check if the property exists in inventoryDetails
|
|
119
|
+
if (inventoryDetails[property]) {
|
|
120
|
+
result = result.replace(tokenPattern, String(inventoryDetails[property]));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Replace with empty string if no match found
|
|
124
|
+
result = result.replace(tokenPattern, '');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Second pass: replace any remaining tokens that weren't in our mapping with empty strings
|
|
128
|
+
// This handles tokens like {?MissingToken} that don't exist in our tokenMappings
|
|
129
|
+
result = result.replace(/\{\?[^}]+\}/g, '');
|
|
130
|
+
return result;
|
|
131
|
+
}
|