feed-common 1.63.0 → 1.64.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable quotes */
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
  /* eslint-disable max-len */
4
- import { dateRangeSchema, measureUnitsSchema, validateIfNoMacro } from './index.js';
4
+ import { dateRangeSchema, measureUnitsSchema, priceSchema, validateIfNoMacro } from './index.js';
5
5
  import { SOURCE_LANGUAGE, XmlFeedFormat } from '../../constants/profile.constants.js';
6
6
  import { ProductUploadMapSource, XmlFeedTemplateType } from '../../types/profile.types.js';
7
7
  import { z } from 'zod';
@@ -39,6 +39,8 @@ const availabilityMapping: ProductUploadMapSource = {
39
39
  rules: { sections: [] },
40
40
  baseMode: true,
41
41
  defaultValue: '',
42
+ singleChoice: true,
43
+ fixedValue: true,
42
44
  };
43
45
 
44
46
  const customLabelMapping: ProductUploadMapSource = {
@@ -102,6 +104,7 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
102
104
  defaultValue: 'https://{{shopify.shop_domain}}/products/{{shopify.handle}}',
103
105
  rules: { sections: [] },
104
106
  baseMode: true,
107
+ singleChoice: true,
105
108
  },
106
109
  {
107
110
  attribute: 'image_link',
@@ -113,6 +116,7 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
113
116
  defaultValue: '{{shopify.image}}',
114
117
  rules: { sections: [] },
115
118
  baseMode: true,
119
+ singleChoice: true,
116
120
  },
117
121
  ...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(() => ({
118
122
  ...(additionalImageLinkMapping as any),
@@ -155,12 +159,12 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
155
159
  },
156
160
  {
157
161
  ...availabilityMapping,
158
- defaultValue: 'in_stock',
162
+ defaultValue: '{{source.google_availability:[{"label": "In Stock", "value": "in_stock"}]}}',
159
163
  rules: { sections: [] },
160
164
  },
161
165
  {
162
166
  ...availabilityMapping,
163
- defaultValue: 'out_of_stock',
167
+ defaultValue: '{{source.google_availability:[{"label": "Out of Stock", "value": "out_of_stock"}]}}',
164
168
  rules: {
165
169
  sections: [
166
170
  {
@@ -201,12 +205,13 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
201
205
  v,
202
206
  z
203
207
  .string()
204
- .refine(v => !v || z.string().date().safeParse(v).success, { message: 'invalid date' })
205
- .refine(v => !v || new Date(v).getTime() + 1000 * 60 * 60 * 24 * 365 > new Date().getTime(), {
208
+ .refine(v => !v || z.string().datetime({ offset: true }).safeParse(v).success, { message: 'invalid date' })
209
+ .refine(v => !v || new Date(v).getTime() < (1000 * 60 * 60 * 24 * 365 + new Date().getTime()), {
206
210
  message: 'The date should be up to one year in the future',
207
211
  })
208
212
  ),
209
- defaultValue: '',
213
+ defaultValue: '{{source.date:}}',
214
+ fixedValue: true,
210
215
  rules: { sections: [] },
211
216
  },
212
217
  {
@@ -217,6 +222,7 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
217
222
  required: false,
218
223
  type: 'macro-input',
219
224
  defaultValue: '',
225
+ validator: (v: string | number) => validateIfNoMacro(v, priceSchema()),
220
226
  rules: { sections: [] },
221
227
  },
222
228
  {
@@ -231,13 +237,14 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
231
237
  v,
232
238
  z
233
239
  .string()
234
- .refine(v => !v || z.string().datetime().safeParse(v).success, { message: 'invalid date' })
235
- .refine(v => !v || new Date(v).getTime() + 1000 * 60 * 60 * 24 * 30 > new Date().getTime(), {
240
+ .refine(v => !v || z.string().datetime({ offset: true }).safeParse(v).success, { message: 'invalid date' })
241
+ .refine(v => !v || new Date(v).getTime() < (1000 * 60 * 60 * 24 * 30 + new Date().getTime()), {
236
242
  message: 'The date should be less than 30 days in the future',
237
243
  })
238
244
  ),
239
- defaultValue: '',
245
+ defaultValue: '{{source.date:}}',
240
246
  rules: { sections: [] },
247
+ fixedValue: true,
241
248
  },
242
249
  // TODO: add condition, checking if sale price is set
243
250
  {
@@ -251,6 +258,7 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
251
258
  defaultValue: '{{shopify.price}} {{shopify.shop_currency}}',
252
259
  rules: { sections: [] },
253
260
  baseMode: true,
261
+ singleChoice: true,
254
262
  },
255
263
  {
256
264
  attribute: 'sale_price',
@@ -270,8 +278,9 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
270
278
  required: false,
271
279
  type: 'macro-input',
272
280
  validator: (v: string | number) => validateIfNoMacro(v, dateRangeSchema),
273
- defaultValue: '',
281
+ defaultValue: '{{source.date_range:}}',
274
282
  rules: { sections: [] },
283
+ fixedValue: true,
275
284
  },
276
285
  {
277
286
  attribute: 'unit_pricing_measure',
@@ -309,6 +318,7 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
309
318
  ),
310
319
  defaultValue: '',
311
320
  rules: { sections: [] },
321
+ singleChoice: true,
312
322
  },
313
323
  {
314
324
  attribute: 'unit_pricing_base_measure',
@@ -354,8 +364,34 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
354
364
  "Details of an installment payment plan. This attribute uses 4 sub-attributes: Months [months] (Required) Integer, the number of installments the buyer has to pay. Amount [amount] (Required) ISO 4217, the amount the buyer has to pay per month. Downpayment [downpayment] (Optional) ISO 4217, the amount the buyer has to pay upfront as a one time payment. Note: if you don't submit the sub-attribute, the default value is 0 or “no down payment”. Credit type [credit_type] (Optional). This sub-attributes uses the following supported values: Finance [finance], Lease [lease]. Example: 6:30 EUR or 6:30 EUR:10 EUR:finance",
355
365
  required: false,
356
366
  type: 'macro-input',
357
- // TODO: add validator
358
- defaultValue: '',
367
+ defaultValue: '{{source.google_instalment:}}',
368
+ fixedValue: true,
369
+ validator: (v: string | number) =>
370
+ validateIfNoMacro(
371
+ v,
372
+ z.string().superRefine((v, ctx) => {
373
+ if (!v) {
374
+ return true;
375
+ }
376
+
377
+ const [months, amount] = v.split(':');
378
+
379
+ if (!z.coerce.number().min(1).int().safeParse(months).success) {
380
+ ctx.addIssue({
381
+ code: z.ZodIssueCode.custom,
382
+ message: 'Months should be a positive whole number',
383
+ });
384
+ }
385
+
386
+ const result = priceSchema().safeParse(amount);
387
+ if (!result.success) {
388
+ ctx.addIssue({
389
+ code: z.ZodIssueCode.custom,
390
+ message: result.error.issues[0].message,
391
+ });
392
+ }
393
+ })
394
+ ),
359
395
  rules: { sections: [] },
360
396
  },
361
397
  {
@@ -365,8 +401,41 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
365
401
  'Details a monthly or annual payment plan that bundles a communications service contract with a wireless product. Optional (available in certain countries for showing wireless products and services only). Syntax: Period [period] (Required), The duration of a single subscription period. This sub-attribute uses the following supported values: Month [month] Year [year]. Period length [period_length] (Required), Integer, the number of subscription periods (months or years) that the buyer must pay. Amount [amount] (Required), ISO 4217, the amount the buyer has to pay per subscription period. Example: month:12:30.00EUR',
366
402
  required: false,
367
403
  type: 'macro-input',
368
- // TODO: add validator
369
- defaultValue: '',
404
+ validator: (v: string | number) =>
405
+ validateIfNoMacro(
406
+ v,
407
+ z.string().superRefine((v, ctx) => {
408
+ if (!v) {
409
+ return true;
410
+ }
411
+
412
+ const [period, periodLength, amount] = v.split(':');
413
+
414
+ if (!['year', 'month'].includes(period)) {
415
+ ctx.addIssue({
416
+ code: z.ZodIssueCode.custom,
417
+ message: 'Period should be either year or month',
418
+ });
419
+ }
420
+
421
+ if (!z.coerce.number().min(1).int().safeParse(periodLength).success) {
422
+ ctx.addIssue({
423
+ code: z.ZodIssueCode.custom,
424
+ message: 'Period length should be a positive whole number',
425
+ });
426
+ }
427
+
428
+ const result = priceSchema().safeParse(amount);
429
+ if (!result.success) {
430
+ ctx.addIssue({
431
+ code: z.ZodIssueCode.custom,
432
+ message: result.error.issues[0].message,
433
+ });
434
+ }
435
+ })
436
+ ),
437
+ defaultValue: '{{source.google_subscription:}}',
438
+ fixedValue: true,
370
439
  rules: { sections: [] },
371
440
  },
372
441
  {
@@ -396,8 +465,10 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
396
465
  'Google-defined product category for your product. Example: Apparel & Accessories > Clothing > Outerwear > Coats & Jackets or 371',
397
466
  required: false,
398
467
  type: 'macro-input',
399
- defaultValue: '',
468
+ defaultValue: '{{source.google_category:}}',
400
469
  rules: { sections: [] },
470
+ fixedValue: true,
471
+ singleChoice: true,
401
472
  },
402
473
  {
403
474
  attribute: 'product_type',
@@ -451,8 +522,10 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
451
522
  required: false,
452
523
  type: 'macro-input',
453
524
  validator: (v: string | number) => validateIfNoMacro(v, z.enum(['yes', 'no', ''])),
454
- defaultValue: 'yes',
525
+ defaultValue: '{{source.yes_no:[{"label": "Yes", "value": "yes"}]}}',
455
526
  rules: { sections: [] },
527
+ fixedValue: true,
528
+ singleChoice: true,
456
529
  },
457
530
  {
458
531
  attribute: 'condition',
@@ -462,8 +535,10 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
462
535
  required: false,
463
536
  type: 'macro-input',
464
537
  validator: (v: string | number) => validateIfNoMacro(v, z.enum(['new', 'refurbished', 'used', ''])),
465
- defaultValue: '',
538
+ defaultValue: '{{source.google_condition:}}',
466
539
  rules: { sections: [] },
540
+ singleChoice: true,
541
+ fixedValue: true,
467
542
  },
468
543
  {
469
544
  attribute: 'adult',
@@ -473,8 +548,10 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
473
548
  required: false,
474
549
  type: 'macro-input',
475
550
  validator: (v: string | number) => validateIfNoMacro(v, z.enum(['yes', 'no', ''])),
476
- defaultValue: '',
551
+ defaultValue: '{{source.yes_no:}]}}',
477
552
  rules: { sections: [] },
553
+ singleChoice: true,
554
+ fixedValue: true,
478
555
  },
479
556
  {
480
557
  attribute: 'multipack',
@@ -495,8 +572,10 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
495
572
  required: false,
496
573
  type: 'macro-input',
497
574
  validator: (v: string | number) => validateIfNoMacro(v, z.enum(['yes', 'no', ''])),
498
- defaultValue: '',
575
+ defaultValue: '{{source.yes_no:}}',
499
576
  rules: { sections: [] },
577
+ singleChoice: true,
578
+ fixedValue: true,
500
579
  },
501
580
  {
502
581
  attribute: 'certification',
@@ -517,8 +596,10 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
517
596
  type: 'macro-input',
518
597
  validator: (v: string | number) =>
519
598
  validateIfNoMacro(v, z.enum(['A', 'A+', 'A++', 'A+++', 'B', 'C', 'D', 'E', 'F', 'G', ''])),
520
- defaultValue: '',
599
+ defaultValue: '{{source.energy_efficiency:}}',
521
600
  rules: { sections: [] },
601
+ singleChoice: true,
602
+ fixedValue: true,
522
603
  },
523
604
  {
524
605
  attribute: 'age_group',
@@ -530,8 +611,10 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
530
611
  // TODO: add to check if product is apparel
531
612
  validator: (v: string | number) =>
532
613
  validateIfNoMacro(v, z.enum(['newborn', 'infant', 'toddler', 'kids', 'adult', ''])),
533
- defaultValue: '',
614
+ defaultValue: '{{source.google_age_group:}}',
534
615
  rules: { sections: [] },
616
+ singleChoice: true,
617
+ fixedValue: true,
535
618
  },
536
619
  {
537
620
  attribute: 'color',
@@ -555,7 +638,9 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
555
638
  // TODO: add to check if product is apparel
556
639
  validator: (v: string | number) => validateIfNoMacro(v, z.enum(['male', 'female', 'unisex', ''])),
557
640
  rules: { sections: [] },
558
- defaultValue: '',
641
+ defaultValue: '{{source.google_gender:}}',
642
+ singleChoice: true,
643
+ fixedValue: true,
559
644
  },
560
645
  {
561
646
  attribute: 'material',
@@ -618,7 +703,8 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
618
703
  )
619
704
  ),
620
705
  rules: { sections: [] },
621
- defaultValue: '',
706
+ defaultValue: '{{source.google_size_type:}}',
707
+ fixedValue: true,
622
708
  },
623
709
  {
624
710
  attribute: 'size_system',
@@ -630,7 +716,9 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
630
716
  validator: (v: string | number) =>
631
717
  validateIfNoMacro(v, z.enum(['US', 'EU', 'UK', 'DE', 'FR', 'JP', 'CN', 'IT', 'BR', 'MEX', 'AU', ''])),
632
718
  rules: { sections: [] },
633
- defaultValue: '',
719
+ defaultValue: '{{source.google_size_system:}}',
720
+ singleChoice: true,
721
+ fixedValue: true,
634
722
  },
635
723
  {
636
724
  attribute: 'item_group_id',
@@ -694,7 +782,14 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
694
782
  "Technical specifications or additional details of your product. This attribute uses 3 sub-attributes: Section name: Max 140 characters. Attribute name: Max 140 characters. Attribute value: Max 1000 characters. Don't add information covered in other attributes, all capital letters, gimmicky foreign characters, promotion text, or list keywords or search terms. Example: General:Product Type:Digital player",
695
783
  required: false,
696
784
  type: 'macro-input',
697
- validator: (v: string | number) => validateIfNoMacro(v, z.string().max(1280)),
785
+ validator: (v: string | number) =>
786
+ validateIfNoMacro(
787
+ v,
788
+ z
789
+ .string()
790
+ .max(1280)
791
+ .refine(v => !v || v.split(':').length === 3, { message: 'Invalid format' })
792
+ ),
698
793
  defaultValue: '',
699
794
  rules: { sections: [] },
700
795
  },
@@ -783,18 +878,29 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
783
878
  validator: (v: string | number) =>
784
879
  validateIfNoMacro(
785
880
  v,
786
- z.enum([
787
- 'Shopping_ads',
788
- 'Buy_on_Google_listings',
789
- 'Display_ads',
790
- 'Local_inventory_ads',
791
- 'Free_listings',
792
- 'Free_local_listings',
793
- 'YouTube_Shopping',
794
- '',
795
- ])
881
+ z
882
+ .string()
883
+ .refine(
884
+ v =>
885
+ !v ||
886
+ v
887
+ .split(',')
888
+ .every((val: string) =>
889
+ [
890
+ 'Shopping_ads',
891
+ 'Buy_on_Google_listings',
892
+ 'Display_ads',
893
+ 'Local_inventory_ads',
894
+ 'Free_listings',
895
+ 'Free_local_listings',
896
+ 'YouTube_Shopping',
897
+ ].includes(val.trim())
898
+ ),
899
+ { message: 'Invalid value' }
900
+ )
796
901
  ),
797
- defaultValue: '',
902
+ defaultValue: '{{source.google_gmc_destinations:}}',
903
+ fixedValue: true,
798
904
  rules: { sections: [] },
799
905
  },
800
906
  {
@@ -807,18 +913,29 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
807
913
  validator: (v: string | number) =>
808
914
  validateIfNoMacro(
809
915
  v,
810
- z.enum([
811
- 'Shopping_ads',
812
- 'Buy_on_Google_listings',
813
- 'Display_ads',
814
- 'Local_inventory_ads',
815
- 'Free_listings',
816
- 'Free_local_listings',
817
- 'YouTube_Shopping',
818
- '',
819
- ])
916
+ z
917
+ .string()
918
+ .refine(
919
+ v =>
920
+ !v ||
921
+ v
922
+ .split(',')
923
+ .every((val: string) =>
924
+ [
925
+ 'Shopping_ads',
926
+ 'Buy_on_Google_listings',
927
+ 'Display_ads',
928
+ 'Local_inventory_ads',
929
+ 'Free_listings',
930
+ 'Free_local_listings',
931
+ 'YouTube_Shopping',
932
+ ].includes(val.trim())
933
+ ),
934
+ { message: 'Invalid value' }
935
+ )
820
936
  ),
821
- defaultValue: '',
937
+ defaultValue: '{{source.google_gmc_destinations:}}',
938
+ fixedValue: true,
822
939
  rules: { sections: [] },
823
940
  },
824
941
  {
@@ -827,9 +944,9 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
827
944
  description:
828
945
  'A setting that allows you to exclude countries where your products are advertised on Shopping ads. 2 characters. Must be an ISO_3166-1_alpha-2 country code. Only available for Shopping ads. Example: DE',
829
946
  required: false,
830
- type: 'select',
831
- choices: [SOURCE_LANGUAGE],
832
- defaultValue: '',
947
+ type: 'macro-input',
948
+ defaultValue: '{{source.country:}}',
949
+ fixedValue: true,
833
950
  rules: { sections: [] },
834
951
  },
835
952
  {
@@ -850,7 +967,8 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
850
967
  "Your product's shipping cost, shipping speeds, and the locations your product ships to. Shipping costs are required for Shopping ads and free listings for the following countries: Australia, Austria, Belgium, Canada, Czechia, France, Germany, India, Ireland, Israel, Italy, New Zealand, Japan, the Netherlands, Poland, Romania, South Korea, Spain, Switzerland, the UK, and the US. This attribute uses the following sub-attributes: Country (Required) ISO 3166 country code. Region (Optional). Postal code (Optional). Location ID (Optional). Location group name (Optional). Service (Optional) Service class or shipping speed. Price (Optional) Fixed shipping cost, including VAT if required. Minimum handling time and maximum handling time (Optional) To specify handling time. Minimum transit time and maximum transit time (Optional) To specify transit time. Supported prices: 0-1000 USD. Example: US:CA:Overnight:16.00 USD:1:1:2:3",
851
968
  required: false,
852
969
  type: 'macro-input',
853
- defaultValue: '',
970
+ defaultValue: '{{source.google_shipping:}}',
971
+ fixedValue: true,
854
972
  rules: { sections: [] },
855
973
  },
856
974
  {
@@ -913,10 +1031,11 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
913
1031
  description:
914
1032
  'A setting that allows you to provide the country from which your product will typically ship. 2 characters. Must be an ISO_3166-1_alpha-2 country code. Example: DE',
915
1033
  required: false,
916
- type: 'select',
917
- choices: [SOURCE_LANGUAGE],
918
- defaultValue: '',
1034
+ type: 'macro-input',
1035
+ defaultValue: '{{source.language:}}',
919
1036
  rules: { sections: [] },
1037
+ singleChoice: true,
1038
+ fixedValue: true,
920
1039
  },
921
1040
  {
922
1041
  attribute: 'max_handling_time',
@@ -958,7 +1077,8 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
958
1077
  'Order cost above which shipping is free. This attribute uses the following sub-attributes: Country (Required) ISO 3166 country code. Price threshold (Required) Order cost above which shipping is free. Example: US:16.00 USD',
959
1078
  required: false,
960
1079
  type: 'macro-input',
961
- defaultValue: '',
1080
+ defaultValue: '{{source.google_free_shipping_threshold:}}',
1081
+ fixedValue: true,
962
1082
  rules: { sections: [] },
963
1083
  },
964
1084
  {
@@ -968,7 +1088,8 @@ export const googleFeedTemplate: XmlFeedTemplateType = {
968
1088
  'Your product’s sales tax rate in percent. Required (Available for the US only). This attribute uses 4 sub-attributes: Country (optional) ISO 3166 country code. Region or postal code or location ID (optional). Rate (required) Tax rate as a percentage. Shipping tax (optional) Specify if you charge tax on shipping, supported values: yes, no. Example: US:CA:5.00:y',
969
1089
  required: false,
970
1090
  type: 'macro-input',
971
- defaultValue: '',
1091
+ defaultValue: '{{source.google_tax:}}',
1092
+ fixedValue: true,
972
1093
  // TODO: add validator to check if US
973
1094
  rules: { sections: [] },
974
1095
  },