Skip to content

Commit 6799fb5

Browse files
levalleux-ludoCopilotCopilot
authored
feat: adapt check exchange policy for bundle offers (#999)
* feat: adapt check exchange policy for bundle offers * fix exchangePolicy name/version/returnPeriod in OfferPolicyDetails when bundle offer * Update packages/react-kit/src/hooks/useCheckExchangePolicy.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/core-sdk/src/offers/checkExchangePolicy.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/core-sdk/src/offers/checkExchangePolicy.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * copilot pr remarks * Update packages/react-kit/src/components/offerPolicy/OfferPolicyDetails.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/react-kit/src/hooks/useCheckExchangePolicy.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * copilot pr remarks * copilot pr remarks * copilot pr remarks * Update packages/core-sdk/src/offers/checkExchangePolicy.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/core-sdk/src/offers/checkExchangePolicy.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/react-kit/src/components/offerPolicy/OfferPolicyDetails.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update data/exchangePolicies/exchangePolicyRules.template.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/core-sdk/src/offers/checkExchangePolicy.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add type guard for Yup ValidationError before accessing inner property (#1000) * Initial plan * Add type checking for Yup ValidationError before accessing inner property Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> * Fix spacing in function call Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> * chore: add defensive error type checking for yup validationError handling (#1001) * Initial plan * Add error type checking for Yup ValidationError handling Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> * Refactor: extract error validation logic into helper function Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> * Improve error property extraction to be more explicit Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> * Improve type safety in error extraction helper Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> * Add comprehensive JSDoc documentation for error extraction helper Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> * fix lint * fix test --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com> Co-authored-by: Ludovic Levalleux <levalleux_ludo@hotmail.com> * copilot pr remarks * some additional fixes * fix render contractual agreement for bundle offer --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: levalleux-ludo <7184124+levalleux-ludo@users.noreply.github.com>
1 parent 9725d6f commit 6799fb5

18 files changed

Lines changed: 2307 additions & 99 deletions

File tree

data/exchangePolicies/exchangePolicyRules.template.json

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"title": "Boson Protocol - Exchange Policy Rules",
55
"description": "Rules to check whether the exchange policy of an offer is fair or not",
66
"type": "object",
7+
"metadataType": "PRODUCT_V1",
78
"properties": {
89
"disputePeriodDuration": {
910
"description": "Dispute Period Duration",
@@ -75,6 +76,97 @@
7576
}
7677
}
7778
},
79+
"yupSchemas": [{
80+
"$schema": "http://json-schema.org/draft-07/schema#",
81+
"title": "Boson Protocol - Exchange Policy Rules",
82+
"description": "Rules to check whether the exchange policy of an offer is fair or not",
83+
"type": "object",
84+
"metadataType": "BUNDLE",
85+
"properties": {
86+
"disputePeriodDuration": {
87+
"description": "Dispute Period Duration",
88+
"type": "number",
89+
"min": 2592000,
90+
"required": true
91+
},
92+
"disputeResolverId": {
93+
"description": "Dispute Resolver",
94+
"type": "string",
95+
"matches": "^(<DEFAULT_DISPUTE_RESOLVER_ID>)$",
96+
"required": true
97+
},
98+
"exchangeToken": {
99+
"type": "object",
100+
"properties": {
101+
"address": {
102+
"description": "Exchange Token",
103+
"type": "string",
104+
"flags": "i",
105+
"pattern": "^(<TOKENS_LIST>)$",
106+
"required": true
107+
}
108+
},
109+
"required": true
110+
},
111+
"resolutionPeriodDuration": {
112+
"description": "Resolution Period Duration",
113+
"type": "number",
114+
"min": 1296000,
115+
"required": true
116+
},
117+
"metadata": {
118+
"type": "object",
119+
"properties": {
120+
"type": {
121+
"description": "Metadata Type",
122+
"type": "string",
123+
"matches": "^BUNDLE$",
124+
"required": true
125+
}
126+
},
127+
"required": true
128+
}
129+
}
130+
}, {
131+
"$schema": "http://json-schema.org/draft-07/schema#",
132+
"title": "Boson Protocol - Exchange Policy Rules",
133+
"description": "Rules to check whether the exchange policy of an offer is fair or not",
134+
"type": "object",
135+
"metadataType": "ITEM_PRODUCT_V1",
136+
"properties": {
137+
"type": {
138+
"description": "Metadata Type",
139+
"type": "string",
140+
"matches": "^ITEM_PRODUCT_V1$",
141+
"required": true
142+
},
143+
"exchangePolicy": {
144+
"type": "object",
145+
"properties": {
146+
"template": {
147+
"description": "Buyer/Seller Agreement Template",
148+
"type": "string",
149+
"flags": "i",
150+
"pattern": "^(fairExchangePolicy|ipfs://QmS6SUVL1mhRq9wyNho914vcHwj3gC491vq7wtdoe34SUz|ipfs://QmZEYfG31PR1SgStg1wCFawQPxtbY9N44vDK9fjj3J9oz2|ipfs://QmXfDShmggHm7BzMbkzv2rRowwPyJ55mypGp32qKSPGto4|ipfs://QmXxRznUVMkQMb6hLiojbiv9uDw22RcEpVk6Gr3YywihcJ|ipfs://QmQ8ZTmmRV15rFaWG9KRyjFRrpaD1o2sDwZoYiWgBaAto6|ipfs://QmaNj7vGuCEvaM5vyucp5z1S9VprMnZWmVxYGn6FHhgePF|ipfs://QmbkoWec4NcmxJk7xpooNyfvj9ZarkW6RXq2ZJ9W6UGXZu|ipfs://QmaUobgQYrMnm2jZ3WowPtwRs4MpMR2TSinp3ChebjnZwe)$",
151+
"required": true
152+
}
153+
},
154+
"required": true
155+
},
156+
"shipping": {
157+
"type": "object",
158+
"properties": {
159+
"returnPeriodInDays": {
160+
"description": "Return Period (in days)",
161+
"type": "number",
162+
"min": 15,
163+
"required": true
164+
}
165+
},
166+
"required": true
167+
}
168+
}
169+
}],
78170
"yupConfig": {
79171
"errMessages": {
80172
"disputePeriodDuration": {
@@ -90,7 +182,7 @@
90182
"min": "Resolution Period Duration is less than 15 days"
91183
},
92184
"type": {
93-
"matches": "Metadata Type is not PRODUCT_V1 standard",
185+
"matches": "Metadata Type is not a supported standard (PRODUCT_V1, BUNDLE or ITEM_PRODUCT_V1)",
94186
"required": "Metadata Type is not specified"
95187
},
96188
"template": {

data/ipfs.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ HERE AFTER THE IPFS HASH OF THE UPLOADED FILES (FOR RECORDING AND TRACEABILITY)
33

44
contractualAgreement.template.md QmaUobgQYrMnm2jZ3WowPtwRs4MpMR2TSinp3ChebjnZwe
55
rNFTLicense.template.md QmPbzbp7xcSKhQPjT5VacLRMVgM1U6DB4LiF2GVyHhvcA7
6-
exchangePolicyRules.template.json QmX8Wnq1eWbf7pRhEDQqdAqWp17YSKXQq8ckZVe4YdqAvt
6+
exchangePolicyRules.template.json QmPBjCyxLdYFGQRJnD1xfdtBTEUsviwJV5Y4ZN3rCBo2QQ

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core-sdk/src/offers/checkExchangePolicy.ts

Lines changed: 138 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ export type YupPropertyDef = {
2727
};
2828

2929
export type CheckExchangePolicyRules = {
30+
// Keep default type for backward compatibility, but also support multiple schemas for different metadata types (e.g., PRODUCT_V1 and BUNDLE)
3031
yupSchema: {
3132
$schema?: string;
3233
$id?: string;
3334
title?: string;
3435
description?: string;
3536
type: string;
37+
metadataType?: string;
3638
properties: {
3739
[key: string]: YupPropertyDef;
3840
};
@@ -49,29 +51,152 @@ export type CheckExchangePolicyRules = {
4951
};
5052
};
5153
};
54+
// Extend the type to support multiple schemas (for different metadata types, e.g., PRODUCT_V1 and BUNDLE)
55+
yupSchemas?: {
56+
$schema?: string;
57+
$id?: string;
58+
title?: string;
59+
description?: string;
60+
type: string;
61+
metadataType: string;
62+
properties: {
63+
[key: string]: YupPropertyDef;
64+
};
65+
}[];
5266
};
5367

68+
/**
69+
* Helper function to safely extract validation errors from a Yup ValidationError.
70+
*
71+
* @param error - The error object to extract validation errors from
72+
* @returns An array of error objects, each containing:
73+
* - message: The error message string
74+
* - path: The path to the invalid field
75+
* - value: The invalid value
76+
*
77+
* Returns an empty array if:
78+
* - The error is not a valid Yup ValidationError
79+
* - The error doesn't have an `inner` property
80+
* - The `inner` property is not an array
81+
*/
82+
function extractValidationErrors(
83+
error: unknown
84+
): Array<{ message: string; path: string; value: unknown }> {
85+
if (
86+
error &&
87+
typeof error === "object" &&
88+
"inner" in error &&
89+
Array.isArray(error.inner)
90+
) {
91+
return error.inner.map((e: unknown) => {
92+
// Extract only the needed properties from the error object
93+
if (e && typeof e === "object") {
94+
return {
95+
message: "message" in e ? String(e.message) : "",
96+
path: (e as { path: string }).path || "",
97+
value: (e as { value?: unknown }).value || undefined
98+
};
99+
}
100+
return { message: "", path: "", value: undefined };
101+
});
102+
}
103+
return [];
104+
}
105+
54106
export function checkExchangePolicy(
55107
offerData: OfferFieldsFragment,
56108
rules: CheckExchangePolicyRules
57109
): CheckExchangePolicyResult {
58-
const baseSchema: Schema<unknown> = buildYup(
59-
rules.yupSchema,
60-
rules.yupConfig
61-
);
110+
let baseSchema: Schema<unknown>;
111+
112+
const metadataType = offerData.metadata?.type;
113+
114+
if (
115+
!rules.yupSchema.metadataType ||
116+
metadataType === rules.yupSchema.metadataType
117+
) {
118+
baseSchema = buildYup(rules.yupSchema, rules.yupConfig);
119+
} else {
120+
// For multiple schemas, use the one matching metadata.type
121+
const rulesTemplate = rules.yupSchemas?.find(
122+
(schema) => schema.metadataType === metadataType
123+
);
124+
125+
if (!rulesTemplate) {
126+
return {
127+
isValid: false,
128+
errors: [
129+
{
130+
message: `Unsupported metadata type: ${String(metadataType)}`,
131+
path: "metadata.type",
132+
value: metadataType
133+
}
134+
]
135+
};
136+
}
137+
138+
baseSchema = buildYup(rulesTemplate, rules.yupConfig);
139+
}
140+
141+
let result = {
142+
isValid: true,
143+
errors: []
144+
};
62145
try {
63146
baseSchema.validateSync(offerData, { abortEarly: false });
64147
} catch (e) {
65-
return {
148+
result = {
66149
isValid: false,
67-
errors:
68-
e.inner?.map((error) => {
69-
return { ...error };
70-
}) || []
150+
errors: extractValidationErrors(e)
71151
};
72152
}
73-
return {
74-
isValid: true,
75-
errors: []
76-
};
153+
if (metadataType === "BUNDLE") {
154+
// For BUNDLE metadata, check each item in the bundle
155+
const bundleItems =
156+
(offerData.metadata as { items?: { type?: string }[] })?.items || [];
157+
if (bundleItems.length === 0) {
158+
// An empty bundle is semantically invalid
159+
result.isValid = false;
160+
result.errors = result.errors.concat([
161+
{
162+
message: "Bundle metadata must contain at least one item.",
163+
path: "metadata.items",
164+
value: bundleItems
165+
}
166+
]);
167+
} else {
168+
for (const item of bundleItems) {
169+
const itemType = item.type;
170+
const itemRulesTemplate = Array.isArray(rules.yupSchemas)
171+
? rules.yupSchemas.find((schema) => schema.metadataType === itemType)
172+
: undefined;
173+
const itemSchema = itemRulesTemplate
174+
? buildYup(itemRulesTemplate, rules.yupConfig)
175+
: undefined;
176+
if (itemSchema) {
177+
try {
178+
itemSchema.validateSync(item, { abortEarly: false });
179+
} catch (e) {
180+
result.isValid = false;
181+
result.errors = result.errors.concat(extractValidationErrors(e));
182+
}
183+
}
184+
}
185+
}
186+
// Ensure bundle contains at least one ITEM_PRODUCT_V1 item,
187+
// as required by the UI logic to extract exchange policy and shipping data.
188+
const hasRequiredProductItem = bundleItems.some(
189+
(item) => item.type === "ITEM_PRODUCT_V1"
190+
);
191+
if (!hasRequiredProductItem) {
192+
result.isValid = false;
193+
result.errors = result.errors.concat({
194+
message:
195+
"Bundle metadata must contain at least one ITEM_PRODUCT_V1 item to provide exchange policy and shipping information.",
196+
path: "metadata.items",
197+
value: bundleItems
198+
});
199+
}
200+
}
201+
return result;
77202
}

packages/core-sdk/src/offers/renderContractualAgreement.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { CreateOfferArgs } from "./types";
99
import {
1010
OfferFieldsFragment,
1111
MetadataType,
12-
ProductV1MetadataEntity
12+
ProductV1MetadataEntity,
13+
ProductV1ItemMetadataEntity,
14+
ItemMetadataType,
15+
BundleMetadataEntity
1316
} from "../subgraph";
1417
import { AddressZero } from "@ethersproject/constants";
1518

@@ -172,6 +175,13 @@ function convertExistingOfferData(offerDataSubGraph: OfferFieldsFragment): {
172175
offerMetadata: AdditionalOfferMetadata;
173176
tokenInfo: ITokenInfo;
174177
} {
178+
const productItemMetadata =
179+
offerDataSubGraph.metadata?.type === MetadataType.BUNDLE
180+
? (offerDataSubGraph.metadata as BundleMetadataEntity).items?.find(
181+
(item): item is ProductV1ItemMetadataEntity =>
182+
item.type === ItemMetadataType.ITEM_PRODUCT_V1
183+
)
184+
: undefined;
175185
return {
176186
offerData: {
177187
...offerDataSubGraph,
@@ -182,6 +192,7 @@ function convertExistingOfferData(offerDataSubGraph: OfferFieldsFragment): {
182192
offerDataSubGraph.voucherRedeemableFromDate,
183193
voucherRedeemableUntilDateInMS:
184194
offerDataSubGraph.voucherRedeemableUntilDate,
195+
voucherValidDurationInMS: offerDataSubGraph.voucherValidDuration,
185196
disputePeriodDurationInMS: offerDataSubGraph.disputePeriodDuration,
186197
resolutionPeriodDurationInMS: offerDataSubGraph.resolutionPeriodDuration,
187198
exchangeToken: offerDataSubGraph.exchangeToken.address,
@@ -197,16 +208,20 @@ function convertExistingOfferData(offerDataSubGraph: OfferFieldsFragment): {
197208
offerDataSubGraph.metadata as ProductV1MetadataEntity
198209
)?.exchangePolicy.sellerContactMethod,
199210
disputeResolverContactMethod: (
200-
offerDataSubGraph.metadata as ProductV1MetadataEntity
211+
productItemMetadata ||
212+
(offerDataSubGraph.metadata as ProductV1MetadataEntity)
201213
)?.exchangePolicy.disputeResolverContactMethod,
202214
escalationDeposit:
203215
offerDataSubGraph.disputeResolutionTerms.buyerEscalationDeposit,
204216
escalationResponsePeriodInSec:
205217
offerDataSubGraph.disputeResolutionTerms.escalationResponsePeriod,
206-
sellerTradingName: (offerDataSubGraph.metadata as ProductV1MetadataEntity)
207-
?.productV1Seller?.name,
218+
sellerTradingName: (
219+
productItemMetadata ||
220+
(offerDataSubGraph.metadata as ProductV1MetadataEntity)
221+
)?.productV1Seller?.name,
208222
returnPeriodInDays: (
209-
offerDataSubGraph.metadata as ProductV1MetadataEntity
223+
productItemMetadata ||
224+
(offerDataSubGraph.metadata as ProductV1MetadataEntity)
210225
)?.shipping.returnPeriodInDays
211226
},
212227
tokenInfo: {

0 commit comments

Comments
 (0)