{"record":{"id":"8718bc7b932049db","repo":"antiwork/gumroad","slug":"responsedata-error-message","errorCode":null,"errorMessage":"responseData.error_message","messagePattern":"responseData\\.error_message","errorType":"exception","errorClass":"ResponseError","httpStatus":null,"severity":"error","filePath":"app/javascript/data/offer_code.ts","lineNumber":141,"sourceCode":"  duration_in_billing_cycles: payload.durationInBillingCycles,\n  minimum_amount_cents: payload.minimumAmount,\n  once_per_cart: payload.discount.type === \"cents\" && payload.oncePerCart,\n  existing_customers_only: payload.existingCustomersOnly,\n  ownership_product_ids: payload.existingCustomersOnly ? payload.ownershipProductIds : [],\n  ownership_duration_tiers: payload.ownershipDurationTiers,\n});\n\nexport const createDiscount = async (payload: DiscountPayload) => {\n  const response = await request({\n    method: \"POST\",\n    accept: \"json\",\n    url: Routes.checkout_discounts_path(),\n    data: buildDiscountPayload(payload),\n  });\n  const responseData = typia.assert<\n    { success: true; offer_codes: OfferCode[]; pagination: PaginationProps } | { success: false; error_message: string }\n  >(await response.json());\n  if (!responseData.success) throw new ResponseError(responseData.error_message);\n  return responseData;\n};\n\nexport const updateDiscount = async (id: string, payload: DiscountPayload) => {\n  const response = await request({\n    method: \"PUT\",\n    accept: \"json\",\n    url: Routes.checkout_discount_path(id),\n    data: buildDiscountPayload(payload),\n  });\n  const responseData = typia.assert<\n    { success: true; offer_codes: OfferCode[]; pagination: PaginationProps } | { success: false; error_message: string }\n  >(await response.json());\n  if (!responseData.success) throw new ResponseError(responseData.error_message);\n  return responseData;\n};\n\nexport const deleteDiscount = async (id: string) => {","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/javascript/data/offer_code.ts#L123-L159","documentation":"createDiscount POSTs checkout_discounts_path; typia.assert narrows the reply, and the { success: false, error_message } branch throws ResponseError carrying the server's error_message. This is the endpoint's validation channel: the discount failed to save and e.message states why in the server's own words.","triggerScenarios":"Offer code already used by another discount; percent value outside 1–100; a non-universal discount with empty selectedProductIds; excluded products overlapping selected ones; currencyCode missing for a universal discount.","commonSituations":"Copying an existing code name; creating a universal percent discount without a currency; excluding variants while selecting their parent product.","solutions":["Show e.message next to the offending form field — the server text names the problem.","Pre-validate client-side: non-empty code, percent 1–100, at least one product selected when not universal.","On a code collision, pick a different code rather than resubmitting.","Verify excludedProductIds and selectedProductIds don't intersect."],"exampleFix":"// before\nawait createDiscount(payload);\n\n// after — catch cheap mistakes before the POST\nif (!payload.code.trim()) throw new Error('Enter a code.');\nif (payload.discount.type === 'percent' && (payload.discount.value <= 0 || payload.discount.value > 100)) throw new Error('Percent off must be between 1 and 100.');\nif (!payload.universal && payload.selectedProductIds.length === 0) throw new Error('Select at least one product.');\nawait createDiscount(payload);","handlingStrategy":"validation","validationCode":"if (!payload.code.trim()) throw new Error('Enter a code.');\nif (payload.discount.type === 'percent' && (payload.discount.value <= 0 || payload.discount.value > 100)) throw new Error('Percent off must be between 1 and 100.');\nif (!payload.universal && payload.selectedProductIds.length === 0) throw new Error('Select at least one product.');\nif (payload.selectedProductIds.some((id) => payload.excludedProductIds.includes(id))) throw new Error('A product cannot be both selected and excluded.');","typeGuard":"const isDiscountFailure = (json: unknown): json is { success: false; error_message: string } =>\n  typeof json === 'object' && json !== null && (json as { success?: unknown }).success === false && typeof (json as { error_message?: unknown }).error_message === 'string';","tryCatchPattern":"try {\n  const result = await createDiscount(payload);\n} catch (e) {\n  assertResponseError(e);\n  showFieldError(e.message); // server's error_message names the offending rule\n}","preventionTips":["Pre-validate code format, percent bounds, and product selection before the POST.","Show error_message at the form field level, not as a toast that scrolls away.","Suggest a unique code when the server reports a collision.","Keep client validation in sync with the server's discount rules."],"tags":["offer-codes","discounts","validation","typia","api-contract"],"backgroundTag":"api-validation-error-message","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}