Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eb59892
feat: adapt check exchange policy for bundle offers
levalleux-ludo Feb 12, 2026
01c42bb
fix exchangePolicy name/version/returnPeriod in OfferPolicyDetails wh…
levalleux-ludo Feb 13, 2026
2460caf
Update packages/react-kit/src/hooks/useCheckExchangePolicy.ts
levalleux-ludo Feb 13, 2026
a6b32e9
Update packages/core-sdk/src/offers/checkExchangePolicy.ts
levalleux-ludo Feb 13, 2026
881389c
Update packages/core-sdk/src/offers/checkExchangePolicy.ts
levalleux-ludo Feb 13, 2026
aab5577
copilot pr remarks
levalleux-ludo Feb 13, 2026
935c5ac
Merge branch '998-check-exchangepolicy-for-bundle' of https://github.…
levalleux-ludo Feb 13, 2026
6a0c3f6
Update packages/react-kit/src/components/offerPolicy/OfferPolicyDetai…
levalleux-ludo Feb 13, 2026
ee0da05
Update packages/react-kit/src/hooks/useCheckExchangePolicy.ts
levalleux-ludo Feb 13, 2026
c0089d0
copilot pr remarks
levalleux-ludo Feb 13, 2026
ecfb612
copilot pr remarks
levalleux-ludo Feb 13, 2026
fe4e9d5
copilot pr remarks
levalleux-ludo Feb 13, 2026
9f040e1
Update packages/core-sdk/src/offers/checkExchangePolicy.ts
levalleux-ludo Feb 13, 2026
a8e40ac
Update packages/core-sdk/src/offers/checkExchangePolicy.ts
levalleux-ludo Feb 13, 2026
cfb67d4
Update packages/react-kit/src/components/offerPolicy/OfferPolicyDetai…
levalleux-ludo Feb 13, 2026
382fd01
Update data/exchangePolicies/exchangePolicyRules.template.json
levalleux-ludo Feb 13, 2026
ee306ce
Update packages/core-sdk/src/offers/checkExchangePolicy.ts
levalleux-ludo Feb 13, 2026
033ea70
Add type guard for Yup ValidationError before accessing inner propert…
Copilot Feb 13, 2026
13a7cae
chore: add defensive error type checking for yup validationError hand…
Copilot Feb 13, 2026
af381c2
copilot pr remarks
levalleux-ludo Feb 13, 2026
b90a328
some additional fixes
levalleux-ludo Feb 13, 2026
a5dfa8d
fix render contractual agreement for bundle offer
levalleux-ludo Feb 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 93 additions & 1 deletion data/exchangePolicies/exchangePolicyRules.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"title": "Boson Protocol - Exchange Policy Rules",
"description": "Rules to check whether the exchange policy of an offer is fair or not",
"type": "object",
"metadataType": "PRODUCT_V1",
"properties": {
"disputePeriodDuration": {
"description": "Dispute Period Duration",
Expand Down Expand Up @@ -75,6 +76,97 @@
}
}
},
"yupSchemas": [{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Boson Protocol - Exchange Policy Rules",
"description": "Rules to check whether the exchange policy of an offer is fair or not",
"type": "object",
"metadataType": "BUNDLE",
"properties": {
"disputePeriodDuration": {
"description": "Dispute Period Duration",
"type": "number",
"min": 2592000,
"required": true
},
"disputeResolverId": {
"description": "Dispute Resolver",
"type": "string",
"matches": "^(<DEFAULT_DISPUTE_RESOLVER_ID>)$",
"required": true
},
"exchangeToken": {
"type": "object",
"properties": {
"address": {
"description": "Exchange Token",
"type": "string",
"flags": "i",
"pattern": "^(<TOKENS_LIST>)$",
"required": true
}
},
"required": true
},
"resolutionPeriodDuration": {
"description": "Resolution Period Duration",
"type": "number",
"min": 1296000,
"required": true
},
"metadata": {
"type": "object",
"properties": {
"type": {
"description": "Metadata Type",
"type": "string",
"matches": "^BUNDLE$",
"required": true
}
},
"required": true
}
}
}, {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Boson Protocol - Exchange Policy Rules",
"description": "Rules to check whether the exchange policy of an offer is fair or not",
"type": "object",
"metadataType": "ITEM_PRODUCT_V1",
"properties": {
"type": {
"description": "Metadata Type",
"type": "string",
"matches": "^ITEM_PRODUCT_V1$",
"required": true
},
"exchangePolicy": {
"type": "object",
"properties": {
"template": {
"description": "Buyer/Seller Agreement Template",
"type": "string",
"flags": "i",
"pattern": "^(fairExchangePolicy|ipfs://QmS6SUVL1mhRq9wyNho914vcHwj3gC491vq7wtdoe34SUz|ipfs://QmZEYfG31PR1SgStg1wCFawQPxtbY9N44vDK9fjj3J9oz2|ipfs://QmXfDShmggHm7BzMbkzv2rRowwPyJ55mypGp32qKSPGto4|ipfs://QmXxRznUVMkQMb6hLiojbiv9uDw22RcEpVk6Gr3YywihcJ|ipfs://QmQ8ZTmmRV15rFaWG9KRyjFRrpaD1o2sDwZoYiWgBaAto6|ipfs://QmaNj7vGuCEvaM5vyucp5z1S9VprMnZWmVxYGn6FHhgePF|ipfs://QmbkoWec4NcmxJk7xpooNyfvj9ZarkW6RXq2ZJ9W6UGXZu|ipfs://QmaUobgQYrMnm2jZ3WowPtwRs4MpMR2TSinp3ChebjnZwe)$",
"required": true
}
},
"required": true
},
"shipping": {
"type": "object",
"properties": {
"returnPeriodInDays": {
"description": "Return Period (in days)",
"type": "number",
"min": 15,
"required": true
}
},
"required": true
}
}
}],
"yupConfig": {
"errMessages": {
"disputePeriodDuration": {
Expand All @@ -90,7 +182,7 @@
"min": "Resolution Period Duration is less than 15 days"
},
"type": {
"matches": "Metadata Type is not PRODUCT_V1 standard",
"matches": "Metadata Type is not a supported standard (PRODUCT_V1, BUNDLE or ITEM_PRODUCT_V1)",
"required": "Metadata Type is not specified"
},
"template": {
Expand Down
2 changes: 1 addition & 1 deletion data/ipfs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ HERE AFTER THE IPFS HASH OF THE UPLOADED FILES (FOR RECORDING AND TRACEABILITY)

contractualAgreement.template.md QmaUobgQYrMnm2jZ3WowPtwRs4MpMR2TSinp3ChebjnZwe
rNFTLicense.template.md QmPbzbp7xcSKhQPjT5VacLRMVgM1U6DB4LiF2GVyHhvcA7
exchangePolicyRules.template.json QmX8Wnq1eWbf7pRhEDQqdAqWp17YSKXQq8ckZVe4YdqAvt
exchangePolicyRules.template.json QmPBjCyxLdYFGQRJnD1xfdtBTEUsviwJV5Y4ZN3rCBo2QQ
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

151 changes: 138 additions & 13 deletions packages/core-sdk/src/offers/checkExchangePolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ export type YupPropertyDef = {
};

export type CheckExchangePolicyRules = {
// Keep default type for backward compatibility, but also support multiple schemas for different metadata types (e.g., PRODUCT_V1 and BUNDLE)
yupSchema: {
$schema?: string;
$id?: string;
title?: string;
description?: string;
type: string;
metadataType?: string;
properties: {
[key: string]: YupPropertyDef;
};
Expand All @@ -49,29 +51,152 @@ export type CheckExchangePolicyRules = {
};
};
};
// Extend the type to support multiple schemas (for different metadata types, e.g., PRODUCT_V1 and BUNDLE)
yupSchemas?: {
$schema?: string;
$id?: string;
title?: string;
description?: string;
type: string;
metadataType: string;
properties: {
[key: string]: YupPropertyDef;
};
}[];
};

/**
* Helper function to safely extract validation errors from a Yup ValidationError.
*
* @param error - The error object to extract validation errors from
* @returns An array of error objects, each containing:
* - message: The error message string
* - path: The path to the invalid field
* - value: The invalid value
*
* Returns an empty array if:
* - The error is not a valid Yup ValidationError
* - The error doesn't have an `inner` property
* - The `inner` property is not an array
*/
function extractValidationErrors(
error: unknown
): Array<{ message: string; path: string; value: unknown }> {
if (
error &&
typeof error === "object" &&
"inner" in error &&
Array.isArray(error.inner)
) {
return error.inner.map((e: unknown) => {
// Extract only the needed properties from the error object
if (e && typeof e === "object") {
return {
message: "message" in e ? String(e.message) : "",
path: (e as { path: string }).path || "",
value: (e as { value?: unknown }).value || undefined
};
}
return { message: "", path: "", value: undefined };
});
}
return [];
}

export function checkExchangePolicy(
offerData: OfferFieldsFragment,
rules: CheckExchangePolicyRules
): CheckExchangePolicyResult {
const baseSchema: Schema<unknown> = buildYup(
rules.yupSchema,
rules.yupConfig
);
let baseSchema: Schema<unknown>;

const metadataType = offerData.metadata?.type;

if (
!rules.yupSchema.metadataType ||
metadataType === rules.yupSchema.metadataType
) {
baseSchema = buildYup(rules.yupSchema, rules.yupConfig);
} else {
// For multiple schemas, use the one matching metadata.type
const rulesTemplate = rules.yupSchemas?.find(
(schema) => schema.metadataType === metadataType
);

if (!rulesTemplate) {
return {
isValid: false,
errors: [
{
message: `Unsupported metadata type: ${String(metadataType)}`,
path: "metadata.type",
value: metadataType
}
]
};
}

baseSchema = buildYup(rulesTemplate, rules.yupConfig);
}

let result = {
isValid: true,
errors: []
};
try {
baseSchema.validateSync(offerData, { abortEarly: false });
} catch (e) {
return {
result = {
isValid: false,
errors:
e.inner?.map((error) => {
return { ...error };
}) || []
errors: extractValidationErrors(e)
};
}
return {
isValid: true,
errors: []
};
if (metadataType === "BUNDLE") {
// For BUNDLE metadata, check each item in the bundle
const bundleItems =
(offerData.metadata as { items?: { type?: string }[] })?.items || [];
if (bundleItems.length === 0) {
// An empty bundle is semantically invalid
result.isValid = false;
result.errors = result.errors.concat([
{
message: "Bundle metadata must contain at least one item.",
path: "metadata.items",
value: bundleItems
}
]);
} else {
for (const item of bundleItems) {
const itemType = item.type;
const itemRulesTemplate = Array.isArray(rules.yupSchemas)
? rules.yupSchemas.find((schema) => schema.metadataType === itemType)
: undefined;
const itemSchema = itemRulesTemplate
? buildYup(itemRulesTemplate, rules.yupConfig)
: undefined;
if (itemSchema) {
try {
itemSchema.validateSync(item, { abortEarly: false });
} catch (e) {
result.isValid = false;
result.errors = result.errors.concat(extractValidationErrors(e));
}
}
}
}
// Ensure bundle contains at least one ITEM_PRODUCT_V1 item,
// as required by the UI logic to extract exchange policy and shipping data.
const hasRequiredProductItem = bundleItems.some(
(item) => item.type === "ITEM_PRODUCT_V1"
);
if (!hasRequiredProductItem) {
result.isValid = false;
result.errors = result.errors.concat({
message:
"Bundle metadata must contain at least one ITEM_PRODUCT_V1 item to provide exchange policy and shipping information.",
path: "metadata.items",
value: bundleItems
});
}
}
return result;
}
25 changes: 20 additions & 5 deletions packages/core-sdk/src/offers/renderContractualAgreement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { CreateOfferArgs } from "./types";
import {
OfferFieldsFragment,
MetadataType,
ProductV1MetadataEntity
ProductV1MetadataEntity,
ProductV1ItemMetadataEntity,
ItemMetadataType,
BundleMetadataEntity
} from "../subgraph";
import { AddressZero } from "@ethersproject/constants";

Expand Down Expand Up @@ -172,6 +175,13 @@ function convertExistingOfferData(offerDataSubGraph: OfferFieldsFragment): {
offerMetadata: AdditionalOfferMetadata;
tokenInfo: ITokenInfo;
} {
const productItemMetadata =
offerDataSubGraph.metadata?.type === MetadataType.BUNDLE
? (offerDataSubGraph.metadata as BundleMetadataEntity).items?.find(
(item): item is ProductV1ItemMetadataEntity =>
item.type === ItemMetadataType.ITEM_PRODUCT_V1
)
: undefined;
return {
offerData: {
...offerDataSubGraph,
Expand All @@ -182,6 +192,7 @@ function convertExistingOfferData(offerDataSubGraph: OfferFieldsFragment): {
offerDataSubGraph.voucherRedeemableFromDate,
voucherRedeemableUntilDateInMS:
offerDataSubGraph.voucherRedeemableUntilDate,
voucherValidDurationInMS: offerDataSubGraph.voucherValidDuration,
disputePeriodDurationInMS: offerDataSubGraph.disputePeriodDuration,
resolutionPeriodDurationInMS: offerDataSubGraph.resolutionPeriodDuration,
exchangeToken: offerDataSubGraph.exchangeToken.address,
Expand All @@ -197,16 +208,20 @@ function convertExistingOfferData(offerDataSubGraph: OfferFieldsFragment): {
offerDataSubGraph.metadata as ProductV1MetadataEntity
)?.exchangePolicy.sellerContactMethod,
disputeResolverContactMethod: (
offerDataSubGraph.metadata as ProductV1MetadataEntity
productItemMetadata ||
(offerDataSubGraph.metadata as ProductV1MetadataEntity)
)?.exchangePolicy.disputeResolverContactMethod,
escalationDeposit:
offerDataSubGraph.disputeResolutionTerms.buyerEscalationDeposit,
escalationResponsePeriodInSec:
offerDataSubGraph.disputeResolutionTerms.escalationResponsePeriod,
sellerTradingName: (offerDataSubGraph.metadata as ProductV1MetadataEntity)
?.productV1Seller?.name,
sellerTradingName: (
productItemMetadata ||
(offerDataSubGraph.metadata as ProductV1MetadataEntity)
)?.productV1Seller?.name,
returnPeriodInDays: (
offerDataSubGraph.metadata as ProductV1MetadataEntity
productItemMetadata ||
(offerDataSubGraph.metadata as ProductV1MetadataEntity)
)?.shipping.returnPeriodInDays
},
tokenInfo: {
Expand Down
Loading
Loading