@thg-altitude/schemaorg 1.0.39 → 1.0.42

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": "@thg-altitude/schemaorg",
3
- "version": "1.0.39",
3
+ "version": "1.0.42",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -11,6 +11,6 @@
11
11
  "author": "Phillip Gourley",
12
12
  "license": "ISC",
13
13
  "peerDependencies": {
14
- "astro": "^4.0.8 || ^5.0.0"
14
+ "astro": "^4.0.8 || ^5.0.0 || ^6.0.0"
15
15
  }
16
16
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { mapProductSchemaDataEnhanced } from '../utils/productSchema.js';
2
+ import { mapProductSchemaDataEnhanced, buildSubscriptionFromContracts } from '../utils/productSchema.js';
3
3
 
4
4
  const {
5
5
  image,
@@ -30,6 +30,9 @@ const {
30
30
  beauty_targetAge,
31
31
  gender,
32
32
  weight,
33
+ subscription, // Optional: explicit subscription pricing data { price, priceCurrency, billingIncrement, unitCode, billingDuration }
34
+ subscriptionConfig, // Optional: config for auto-detecting from subscriptionContracts on variants
35
+ // { discountPercentage: 21, frequencyUnit: "MONTH", frequencyValue: 1 }
33
36
  colorVariantPages, // Array of {url, sku, color} for sibling color pages
34
37
  currentColor, // The color of the current page (used to filter variants)
35
38
  baseProductGroupID, // Optional: explicit base product group ID without color suffix
@@ -386,6 +389,26 @@ function generateProductSchema() {
386
389
  },
387
390
  ]
388
391
  : []),
392
+ ...(() => {
393
+ // Subscription pricing: subscriptionContracts + config, or explicit subscription prop
394
+ const variantPrice = parseFloat(variant.price.price.amount);
395
+ let effectiveSub;
396
+ if (variant.subscriptionContracts?.length > 0 && subscriptionConfig) {
397
+ effectiveSub = buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, currency);
398
+ } else if (subscription) {
399
+ effectiveSub = { ...subscription };
400
+ }
401
+ if (!effectiveSub) return [];
402
+ return [{
403
+ "@type": "UnitPriceSpecification",
404
+ priceComponentType: "https://schema.org/Subscription",
405
+ ...(effectiveSub.price != null && { price: effectiveSub.price }),
406
+ ...(effectiveSub.priceCurrency && { priceCurrency: effectiveSub.priceCurrency }),
407
+ ...(effectiveSub.billingIncrement != null && { billingIncrement: effectiveSub.billingIncrement }),
408
+ ...(effectiveSub.unitCode && { unitCode: effectiveSub.unitCode }),
409
+ ...(effectiveSub.billingDuration != null && { billingDuration: effectiveSub.billingDuration }),
410
+ }];
411
+ })(),
389
412
  ],
390
413
  }
391
414
  };
@@ -489,6 +512,26 @@ function generateProductSchema() {
489
512
  },
490
513
  ]
491
514
  : []),
515
+ ...(() => {
516
+ // Subscription pricing: subscriptionContracts + config, or explicit subscription prop
517
+ const variantPrice = parseFloat(variant.price.price.amount);
518
+ let effectiveSub;
519
+ if (variant.subscriptionContracts?.length > 0 && subscriptionConfig) {
520
+ effectiveSub = buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, currency);
521
+ } else if (subscription) {
522
+ effectiveSub = { ...subscription };
523
+ }
524
+ if (!effectiveSub) return [];
525
+ return [{
526
+ "@type": "UnitPriceSpecification",
527
+ priceComponentType: "https://schema.org/Subscription",
528
+ ...(effectiveSub.price != null && { price: effectiveSub.price }),
529
+ ...(effectiveSub.priceCurrency && { priceCurrency: effectiveSub.priceCurrency }),
530
+ ...(effectiveSub.billingIncrement != null && { billingIncrement: effectiveSub.billingIncrement }),
531
+ ...(effectiveSub.unitCode && { unitCode: effectiveSub.unitCode }),
532
+ ...(effectiveSub.billingDuration != null && { billingDuration: effectiveSub.billingDuration }),
533
+ }];
534
+ })(),
492
535
  ],
493
536
  };
494
537
  });
@@ -1,9 +1,16 @@
1
1
  ---
2
- import { mapProductSchemaData, stripHtml } from '../utils/productSchema.js';
2
+ import { mapProductSchemaData, buildSubscriptionFromContracts, stripHtml } from '../utils/productSchema.js';
3
3
 
4
4
  // Use the mapping function to extract data from the full product data
5
5
  const mappedData = mapProductSchemaData(Astro.props);
6
6
 
7
+ // Subscription data resolution:
8
+ // 1. Explicit subscription prop: { price, priceCurrency, billingIncrement, unitCode, billingDuration }
9
+ // 2. Auto-detect from variant.subscriptionContracts + subscriptionConfig prop
10
+ // subscriptionConfig: { discountPercentage, frequencyUnit, frequencyValue, billingDuration }
11
+ const subscription = Astro.props.subscription || null;
12
+ const subscriptionConfig = Astro.props.subscriptionConfig;
13
+
7
14
  // Extract suggestedGender and suggestedAge using the comprehensive mapping function
8
15
  let suggestedGender = Astro.props.suggestedGender || mappedData.suggestedGender;
9
16
  let suggestedAge = Astro.props.suggestedAge || mappedData.suggestedAge;
@@ -253,6 +260,39 @@ if (hasMultipleVariants) {
253
260
 
254
261
  if (variant?.price?.price?.amount) offerObj.price = parseFloat(variant.price.price.amount);
255
262
  if (Astro.props.currency) offerObj.priceCurrency = Astro.props.currency;
263
+
264
+ // Add subscription pricing:
265
+ // 1. subscriptionContracts + subscriptionConfig (subscribe & gain / auto-replenish)
266
+ // 2. Explicit subscription prop
267
+ // 3. Box subscription variant (isSubscription + subscriptionTerm/subscriptionFrequency)
268
+ const variantPrice = variant?.price?.price?.amount ? parseFloat(variant.price.price.amount) : null;
269
+ let effectiveSub;
270
+ if (variant.subscriptionContracts?.length > 0 && subscriptionConfig) {
271
+ effectiveSub = buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, Astro.props.currency);
272
+ } else if (variant.isSubscription) {
273
+ // Box subscription variant - build from variant's own subscription fields
274
+ effectiveSub = {
275
+ price: variantPrice,
276
+ priceCurrency: Astro.props.currency || null,
277
+ billingIncrement: variant.subscriptionFrequency ?? 1,
278
+ unitCode: 'MON',
279
+ billingDuration: variant.subscriptionTerm ?? null,
280
+ };
281
+ } else if (subscription) {
282
+ effectiveSub = { ...subscription };
283
+ }
284
+ if (effectiveSub) {
285
+ const subscriptionSpec = {
286
+ "@type": "UnitPriceSpecification",
287
+ "priceComponentType": "https://schema.org/Subscription"
288
+ };
289
+ if (effectiveSub.price != null) subscriptionSpec.price = effectiveSub.price;
290
+ if (effectiveSub.priceCurrency) subscriptionSpec.priceCurrency = effectiveSub.priceCurrency;
291
+ if (effectiveSub.billingIncrement != null) subscriptionSpec.billingIncrement = effectiveSub.billingIncrement;
292
+ if (effectiveSub.unitCode) subscriptionSpec.unitCode = effectiveSub.unitCode;
293
+ if (effectiveSub.billingDuration != null) subscriptionSpec.billingDuration = effectiveSub.billingDuration;
294
+ offerObj.priceSpecification = subscriptionSpec;
295
+ }
256
296
 
257
297
  variantObj.offers = offerObj;
258
298
  variantsArray.push(variantObj);
@@ -359,6 +399,39 @@ if (hasMultipleVariants) {
359
399
 
360
400
  if (variant?.price?.price?.amount) offerObj.price = parseFloat(variant.price.price.amount);
361
401
  if (Astro.props.currency) offerObj.priceCurrency = Astro.props.currency;
402
+
403
+ // Add subscription pricing:
404
+ // 1. subscriptionContracts + subscriptionConfig (subscribe & gain / auto-replenish)
405
+ // 2. Box subscription variant (isSubscription + subscriptionTerm/subscriptionFrequency)
406
+ // 3. Explicit subscription prop
407
+ const variantPrice = variant?.price?.price?.amount ? parseFloat(variant.price.price.amount) : null;
408
+ let effectiveSub;
409
+ if (variant.subscriptionContracts?.length > 0 && subscriptionConfig) {
410
+ effectiveSub = buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, Astro.props.currency);
411
+ } else if (variant.isSubscription) {
412
+ // Box subscription variant - build from variant's own subscription fields
413
+ effectiveSub = {
414
+ price: variantPrice,
415
+ priceCurrency: Astro.props.currency || null,
416
+ billingIncrement: variant.subscriptionFrequency ?? 1,
417
+ unitCode: 'MON',
418
+ billingDuration: variant.subscriptionTerm ?? null,
419
+ };
420
+ } else if (subscription) {
421
+ effectiveSub = { ...subscription };
422
+ }
423
+ if (effectiveSub) {
424
+ const subscriptionSpec = {
425
+ "@type": "UnitPriceSpecification",
426
+ "priceComponentType": "https://schema.org/Subscription"
427
+ };
428
+ if (effectiveSub.price != null) subscriptionSpec.price = effectiveSub.price;
429
+ if (effectiveSub.priceCurrency) subscriptionSpec.priceCurrency = effectiveSub.priceCurrency;
430
+ if (effectiveSub.billingIncrement != null) subscriptionSpec.billingIncrement = effectiveSub.billingIncrement;
431
+ if (effectiveSub.unitCode) subscriptionSpec.unitCode = effectiveSub.unitCode;
432
+ if (effectiveSub.billingDuration != null) subscriptionSpec.billingDuration = effectiveSub.billingDuration;
433
+ offerObj.priceSpecification = subscriptionSpec;
434
+ }
362
435
 
363
436
  offersArray.push(offerObj);
364
437
  }
@@ -463,4 +536,4 @@ if (reviewsArray.length > 0) schema.review = reviewsArray;
463
536
  return undefined;
464
537
  }
465
538
  return value;
466
- })}></script>
539
+ })}></script>
@@ -87,6 +87,66 @@ export function mapProductSchemaDataEnhanced(productData) {
87
87
  return mapProductSchemaData(productData);
88
88
  }
89
89
 
90
+ /**
91
+ * Checks if a variant has subscription contracts and builds subscription data
92
+ * from the variant's subscriptionContracts array combined with a subscriptionConfig.
93
+ *
94
+ * The subscriptionConfig provides the discount/frequency details that aren't
95
+ * available on the contract objects themselves.
96
+ *
97
+ * @param {Object} variant - The variant object with subscriptionContracts and price data
98
+ * @param {Object} subscriptionConfig - Config with discount/frequency defaults:
99
+ * {
100
+ * discountPercentage: 21, // % discount for subscription (applied to RRP when available)
101
+ * frequencyUnit: "MONTH", // MONTH, WEEK, DAY, YEAR
102
+ * frequencyValue: 1, // billing increment (1 = every month)
103
+ * billingDuration: null, // null = ongoing
104
+ * }
105
+ * @param {number} variantPrice - The variant's current/sale price (fallback if no RRP)
106
+ * @param {string} currency - Currency code (e.g. "GBP")
107
+ * @returns {Object|null} Mapped subscription data or null
108
+ */
109
+ export function buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, currency) {
110
+ if (!variant?.subscriptionContracts || variant?.subscriptionContracts?.length === 0) return null;
111
+ if (!subscriptionConfig) return null;
112
+
113
+ const discountPct = subscriptionConfig.discountPercentage ?? 0;
114
+
115
+ // Use RRP as the base price for discount calculation when available
116
+ // The subscription discount is typically applied to the RRP (full price), not the sale price
117
+ const rrpPrice = variant?.price?.rrp?.amount ? parseFloat(variant.price.rrp.amount) : null;
118
+ const basePrice = rrpPrice ?? variantPrice;
119
+
120
+ // Compute discounted price from the base price (RRP or sale price)
121
+ let subscriptionPrice = null;
122
+ if (basePrice != null && discountPct > 0) {
123
+ subscriptionPrice = parseFloat((basePrice * (1 - discountPct / 100)).toFixed(2));
124
+ }
125
+
126
+ // Map frequency unit
127
+ const frequencyUnit = (subscriptionConfig.frequencyUnit || 'MONTH').toUpperCase();
128
+ const unitCodeMap = {
129
+ 'MONTH': 'MON',
130
+ 'WEEK': 'WEE',
131
+ 'DAY': 'DAY',
132
+ 'YEAR': 'ANN',
133
+ };
134
+ const unitCode = unitCodeMap[frequencyUnit] || frequencyUnit;
135
+
136
+ const billingIncrement = subscriptionConfig.frequencyValue ?? 1;
137
+
138
+ return {
139
+ price: subscriptionPrice,
140
+ priceCurrency: currency || null,
141
+ billingIncrement,
142
+ unitCode,
143
+ billingDuration: subscriptionConfig.billingDuration ?? null,
144
+ initialDiscountPercentage: discountPct,
145
+ recurringDiscountPercentage: subscriptionConfig.recurringDiscountPercentage ?? discountPct,
146
+ contractIds: variant.subscriptionContracts.map(c => c.id),
147
+ };
148
+ }
149
+
90
150
  /**
91
151
  * Simple function to strip HTML tags from text
92
152
  */