From 0461d97640f3d53b174196334d0bcc7b346fe3eb Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Mon, 16 Oct 2023 17:11:17 +0200 Subject: [PATCH 01/13] commerce wrapper - POC - WIP --- composable-ui/next.config.js | 1 + composable-ui/package.json | 1 + .../src/server/data-source/commerce.ts | 4 +- docs/docs/essentials/monorepo.md | 1 + package.json | 3 + packages/types/src/commerce/cart.ts | 25 +++++ packages/voucherify/.eslintrc.js | 4 + packages/voucherify/index.ts | 1 + packages/voucherify/package.json | 20 ++++ packages/voucherify/src/index.ts | 31 ++++++ packages/voucherify/tsconfig.json | 8 ++ pnpm-lock.yaml | 102 ++++++++++++++++-- 12 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 packages/voucherify/.eslintrc.js create mode 100644 packages/voucherify/index.ts create mode 100644 packages/voucherify/package.json create mode 100644 packages/voucherify/src/index.ts create mode 100644 packages/voucherify/tsconfig.json diff --git a/composable-ui/next.config.js b/composable-ui/next.config.js index 3477a86..6234abd 100644 --- a/composable-ui/next.config.js +++ b/composable-ui/next.config.js @@ -34,6 +34,7 @@ module.exports = () => { transpilePackages: [ '@composable/cms-generic', '@composable/commerce-generic', + '@composable/voucherify', '@composable/stripe', '@composable/types', '@composable/ui', diff --git a/composable-ui/package.json b/composable-ui/package.json index 0f48014..a4bfe26 100644 --- a/composable-ui/package.json +++ b/composable-ui/package.json @@ -21,6 +21,7 @@ "@chakra-ui/theme-tools": "^2.0.16", "@composable/cms-generic": "workspace:*", "@composable/commerce-generic": "workspace:*", + "@composable/voucherify": "workspace:*", "@composable/stripe": "workspace:*", "@composable/types": "workspace:*", "@composable/ui": "workspace:*", diff --git a/composable-ui/src/server/data-source/commerce.ts b/composable-ui/src/server/data-source/commerce.ts index d4dcfbf..96eb9e8 100644 --- a/composable-ui/src/server/data-source/commerce.ts +++ b/composable-ui/src/server/data-source/commerce.ts @@ -1,2 +1,4 @@ import { commerceGenericDataSource } from '@composable/commerce-generic' -export default commerceGenericDataSource +import { commerceWithDiscount } from '@composable/voucherify' + +export default commerceWithDiscount(commerceGenericDataSource) diff --git a/docs/docs/essentials/monorepo.md b/docs/docs/essentials/monorepo.md index 0438690..9515a23 100644 --- a/docs/docs/essentials/monorepo.md +++ b/docs/docs/essentials/monorepo.md @@ -38,6 +38,7 @@ The following table lists the packages exported by the mono-repository: | - | - | | `@composable/cms-generic` | `packages/cms-generic` | | `@composable/commerce-generic`| `packages/commerce-generic` | +| `@composable/voucherify`| `packages/voucherify` | | `@composable/eslint-config-custom` | `packages/eslint-config-custom` | | `@composable/stripe` | `packages/stripe` | | `@composable/tsconfig` | `packages/tsconfig` | diff --git a/package.json b/package.json index 13e25cc..3621203 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,8 @@ }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" + }, + "dependencies": { + "@voucherify/sdk": "^2.5.0" } } diff --git a/packages/types/src/commerce/cart.ts b/packages/types/src/commerce/cart.ts index 7e7804e..a29f42e 100644 --- a/packages/types/src/commerce/cart.ts +++ b/packages/types/src/commerce/cart.ts @@ -29,3 +29,28 @@ export interface CartItem { sku: string slug: string } + +// Extended cart related types by Voucherify discounts + +type CartItemWithDiscounts = CartItem & { + cartItemType: 'CartItemWithDiscounts' + discounts: { + subtotal_amount: string // Final order item amount after the applied item-level discount. If there are no item-level discounts applied + } +} + +type CartWithDiscounts = Cart & { + cartType: 'CartWithDiscounts' + redeemables: Redeemable[] + summary: { + discountAmount: string // Sum of all order-level discounts applied to the order. + totalDiscountAmount: string // Sum of all order-level AND all product-specific discounts applied to the order. + grandPrice: string // Order amount after applying all the discounts. + } +} + +type Redeemable = { + id: string + status: string + object: string +} diff --git a/packages/voucherify/.eslintrc.js b/packages/voucherify/.eslintrc.js new file mode 100644 index 0000000..b56159e --- /dev/null +++ b/packages/voucherify/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['custom'], +} diff --git a/packages/voucherify/index.ts b/packages/voucherify/index.ts new file mode 100644 index 0000000..6f39cd4 --- /dev/null +++ b/packages/voucherify/index.ts @@ -0,0 +1 @@ +export * from './src' diff --git a/packages/voucherify/package.json b/packages/voucherify/package.json new file mode 100644 index 0000000..c364b3d --- /dev/null +++ b/packages/voucherify/package.json @@ -0,0 +1,20 @@ +{ + "name": "@composable/voucherify", + "version": "0.0.0", + "main": "./index.ts", + "types": "./index.ts", + "sideEffects": "false", + "scripts": { + "build": "echo \"Build script for @composable/voucherify ...\"", + "lint": "eslint \"**/*.{js,ts,tsx}\" --max-warnings 0", + "ts": "tsc --noEmit --incremental" + }, + "dependencies": { + "@composable/types": "workspace:*" + }, + "devDependencies": { + "eslint-config-custom": "workspace:*", + "tsconfig": "workspace:*", + "typescript": "^4.5.5" + } +} diff --git a/packages/voucherify/src/index.ts b/packages/voucherify/src/index.ts new file mode 100644 index 0000000..4576d2f --- /dev/null +++ b/packages/voucherify/src/index.ts @@ -0,0 +1,31 @@ +import { CommerceService } from '@composable/types' + +const hasKey = (obj: T, k: keyof any): k is keyof T => + k in obj + +export const commerceWithDiscount = (commerceService: CommerceService) => { + return commerceService +} +// export const commerceWithDiscount = (commerceService: CommerceService) => { + +// return new Proxy(commerceService, { +// get: function (target, prop) { + +// if(prop === 'getCart'){ +// return async (...props: Parameters) => { +// console.log('get cart items params', props) + +// const cart = await target.getCart(...props) + +// console.log('result', cart) + +// return new Promise(resolve => cart) +// } +// } + +// return hasKey(target, prop) ? target[prop] : () => { +// throw new Error('Function deos not exists') +// }; +// } +// }); +// } diff --git a/packages/voucherify/tsconfig.json b/packages/voucherify/tsconfig.json new file mode 100644 index 0000000..6b51fef --- /dev/null +++ b/packages/voucherify/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/react-library.json", + "include": ["."], + "exclude": [".turbo", "dist", "tmp", "node_modules", "tsconfig.tsbuildinfo"], + "compilerOptions": { + "resolveJsonModule": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec4b212..d9fc17d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,8 +1,16 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: .: + dependencies: + '@voucherify/sdk': + specifier: ^2.5.0 + version: 2.5.0 devDependencies: '@babel/core': specifier: ^7.0.0 @@ -46,6 +54,9 @@ importers: '@composable/ui': specifier: workspace:* version: link:../packages/ui + '@composable/voucherify': + specifier: workspace:* + version: link:../packages/voucherify '@emotion/react': specifier: ^11.9.3 version: 11.10.6(@types/react@18.0.31)(react@18.2.0) @@ -228,7 +239,7 @@ importers: version: 8.8.0(eslint@7.32.0) eslint-config-turbo: specifier: latest - version: 1.8.8(eslint@7.32.0) + version: 1.10.15(eslint@7.32.0) eslint-plugin-react: specifier: 7.28.0 version: 7.28.0(eslint@7.32.0) @@ -333,6 +344,22 @@ importers: specifier: ^4.5.5 version: 4.9.5 + packages/voucherify: + dependencies: + '@composable/types': + specifier: workspace:* + version: link:../types + devDependencies: + eslint-config-custom: + specifier: workspace:* + version: link:../eslint-config-custom + tsconfig: + specifier: workspace:* + version: link:../tsconfig + typescript: + specifier: ^4.5.5 + version: 4.9.5 + scripts: dependencies: algoliasearch: @@ -3252,6 +3279,7 @@ packages: /@emotion/memoize@0.7.4: resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + requiresBuild: true optional: true /@emotion/memoize@0.8.0: @@ -6605,6 +6633,16 @@ packages: resolution: {integrity: sha512-haGBC8noyA5BfjCRXRH+VIkHCDVW5iD5UX24P2nOdilwUxI4qWsattS/co8QBGq64XsNLRAMdM5pQUE3zxkF9Q==} dev: true + /@voucherify/sdk@2.5.0: + resolution: {integrity: sha512-qRi9lfP/kshsmi+An1cRIWrA9NDfj9tqDFSjvhpnm3BhuxlZnp2SgNgiUejTDkoMNxDgSTH7vImC1w9f51WOgw==} + dependencies: + axios: 0.27.2 + form-data: 4.0.0 + qs: 6.9.7 + transitivePeerDependencies: + - debug + dev: false + /@webassemblyjs/ast@1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} dependencies: @@ -7196,6 +7234,7 @@ packages: /array-find-index@1.0.2: resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -7330,6 +7369,7 @@ packages: /async-each@1.0.6: resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==} + requiresBuild: true dev: false optional: true @@ -7377,6 +7417,15 @@ packages: - debug dev: false + /axios@0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@3.1.1: resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} dependencies: @@ -7607,6 +7656,7 @@ packages: /big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} engines: {node: '>=0.6'} + requiresBuild: true dev: false optional: true @@ -7617,6 +7667,7 @@ packages: /binary-extensions@1.13.1: resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -7626,6 +7677,7 @@ packages: /bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + requiresBuild: true dependencies: file-uri-to-path: 1.0.0 dev: false @@ -7689,6 +7741,7 @@ packages: /bplist-parser@0.1.1: resolution: {integrity: sha512-2AEM0FXy8ZxVLBuqX0hqt1gDwcnz2zygEkQ6zaD5Wko/sB9paUNwlpawrFtKeHUAQUOzjVy9AO4oeonqIHKA9Q==} + requiresBuild: true dependencies: big-integer: 1.6.51 dev: false @@ -7965,6 +8018,7 @@ packages: /camelcase-keys@2.1.0: resolution: {integrity: sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: camelcase: 2.1.1 map-obj: 1.0.1 @@ -7974,6 +8028,7 @@ packages: /camelcase@2.1.1: resolution: {integrity: sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -8047,6 +8102,7 @@ packages: /chokidar@2.1.8: resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==} deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies + requiresBuild: true dependencies: anymatch: 2.0.0 async-each: 1.0.6 @@ -8767,6 +8823,7 @@ packages: /currently-unhandled@0.4.1: resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: array-find-index: 1.0.2 dev: false @@ -8826,6 +8883,7 @@ packages: /decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -9431,13 +9489,13 @@ packages: eslint: 7.32.0 dev: false - /eslint-config-turbo@1.8.8(eslint@7.32.0): - resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==} + /eslint-config-turbo@1.10.15(eslint@7.32.0): + resolution: {integrity: sha512-76mpx2x818JZE26euen14utYcFDxOahZ9NaWA+6Xa4pY2ezVKVschuOxS96EQz3o3ZRSmcgBOapw/gHbN+EKxQ==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 7.32.0 - eslint-plugin-turbo: 1.8.8(eslint@7.32.0) + eslint-plugin-turbo: 1.10.15(eslint@7.32.0) dev: false /eslint-import-resolver-node@0.3.7: @@ -9612,11 +9670,12 @@ packages: string.prototype.matchall: 4.0.8 dev: false - /eslint-plugin-turbo@1.8.8(eslint@7.32.0): - resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==} + /eslint-plugin-turbo@1.10.15(eslint@7.32.0): + resolution: {integrity: sha512-Tv4QSKV/U56qGcTqS/UgOvb9HcKFmWOQcVh3HEaj7of94lfaENgfrtK48E2CckQf7amhKs1i+imhCsNCKjkQyA==} peerDependencies: eslint: '>6.6.0' dependencies: + dotenv: 16.0.3 eslint: 7.32.0 dev: false @@ -10002,6 +10061,7 @@ packages: /file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + requiresBuild: true dev: false optional: true @@ -10060,6 +10120,7 @@ packages: /find-up@1.1.2: resolution: {integrity: sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: path-exists: 2.1.0 pinkie-promise: 2.0.1 @@ -10256,7 +10317,6 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - dev: true /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -10448,6 +10508,7 @@ packages: /get-stdin@4.0.1: resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -11099,6 +11160,7 @@ packages: /indent-string@2.1.0: resolution: {integrity: sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: repeating: 2.0.1 dev: false @@ -11299,6 +11361,7 @@ packages: /is-binary-path@1.0.1: resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: binary-extensions: 1.13.1 dev: false @@ -11414,6 +11477,7 @@ packages: /is-finite@1.1.0: resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -11590,6 +11654,7 @@ packages: /is-utf8@0.2.1: resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} + requiresBuild: true dev: false optional: true @@ -12541,6 +12606,7 @@ packages: /load-json-file@1.1.0: resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: graceful-fs: 4.2.11 parse-json: 2.2.0 @@ -12676,6 +12742,7 @@ packages: /loud-rejection@1.6.0: resolution: {integrity: sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: currently-unhandled: 0.4.1 signal-exit: 3.0.7 @@ -12756,6 +12823,7 @@ packages: /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -12859,6 +12927,7 @@ packages: /meow@3.7.0: resolution: {integrity: sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: camelcase-keys: 2.1.0 decamelize: 1.2.0 @@ -13133,6 +13202,7 @@ packages: /nan@2.17.0: resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==} + requiresBuild: true dev: false optional: true @@ -13717,6 +13787,7 @@ packages: /os-homedir@1.0.2: resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -13880,6 +13951,7 @@ packages: /parse-json@2.2.0: resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: error-ex: 1.3.2 dev: false @@ -13931,11 +14003,13 @@ packages: /path-dirname@1.0.2: resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} + requiresBuild: true dev: false /path-exists@2.1.0: resolution: {integrity: sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: pinkie-promise: 2.0.1 dev: false @@ -13973,6 +14047,7 @@ packages: /path-type@1.1.0: resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: graceful-fs: 4.2.11 pify: 2.3.0 @@ -14030,6 +14105,7 @@ packages: /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -14046,6 +14122,7 @@ packages: /pinkie-promise@2.0.1: resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: pinkie: 2.0.4 dev: false @@ -14054,6 +14131,7 @@ packages: /pinkie@2.0.4: resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -14859,6 +14937,7 @@ packages: /read-pkg-up@1.0.1: resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: find-up: 1.1.2 read-pkg: 1.1.0 @@ -14877,6 +14956,7 @@ packages: /read-pkg@1.1.0: resolution: {integrity: sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: load-json-file: 1.1.0 normalize-package-data: 2.5.0 @@ -14917,6 +14997,7 @@ packages: /readdirp@2.2.1: resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} engines: {node: '>=0.10'} + requiresBuild: true dependencies: graceful-fs: 4.2.11 micromatch: 3.1.10 @@ -14935,6 +15016,7 @@ packages: /redent@1.0.0: resolution: {integrity: sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: indent-string: 2.1.0 strip-indent: 1.0.1 @@ -15154,6 +15236,7 @@ packages: /repeating@2.0.1: resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: is-finite: 1.1.0 dev: false @@ -16034,6 +16117,7 @@ packages: /strip-bom@2.0.0: resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: is-utf8: 0.2.1 dev: false @@ -16061,6 +16145,7 @@ packages: resolution: {integrity: sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==} engines: {node: '>=0.10.0'} hasBin: true + requiresBuild: true dependencies: get-stdin: 4.0.1 dev: false @@ -16462,6 +16547,7 @@ packages: /trim-newlines@1.0.0: resolution: {integrity: sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -16936,6 +17022,7 @@ packages: /untildify@2.1.0: resolution: {integrity: sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==} engines: {node: '>=0.10.0'} + requiresBuild: true dependencies: os-homedir: 1.0.2 dev: false @@ -16944,6 +17031,7 @@ packages: /upath@1.2.0: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} + requiresBuild: true dev: false optional: true From c56b76373972c117c1675ff89ca4a3e4971587f6 Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Tue, 17 Oct 2023 16:26:20 +0200 Subject: [PATCH 02/13] wrap commerce-generic and validate cart --- .../src/components/cart/cart-summary.tsx | 30 +++ composable-ui/src/hooks/use-cart.ts | 2 +- composable-ui/src/server/intl/en-US.json | 2 + packages/types/index.ts | 1 + packages/types/src/commerce/cart.ts | 25 --- .../src/voucherify/cart-with-discounts.ts | 37 ++++ .../commerce-service-with-discounts.ts | 18 ++ packages/types/src/voucherify/index.ts | 2 + packages/voucherify/src/index.ts | 209 ++++++++++++++++-- 9 files changed, 278 insertions(+), 48 deletions(-) create mode 100644 packages/types/src/voucherify/cart-with-discounts.ts create mode 100644 packages/types/src/voucherify/commerce-service-with-discounts.ts create mode 100644 packages/types/src/voucherify/index.ts diff --git a/composable-ui/src/components/cart/cart-summary.tsx b/composable-ui/src/components/cart/cart-summary.tsx index b51a87a..ac4809a 100644 --- a/composable-ui/src/components/cart/cart-summary.tsx +++ b/composable-ui/src/components/cart/cart-summary.tsx @@ -90,6 +90,36 @@ export const CartSummary = ({ )} + + {_cartData.summary?.totalDiscountAmount && ( + + + + )} + + {_cartData.summary?.grandPrice && ( + <> + + + + {intl.formatMessage({ id: 'cart.summary.grandPrice' })} + + + + + + + )} diff --git a/composable-ui/src/hooks/use-cart.ts b/composable-ui/src/hooks/use-cart.ts index 79a2885..6077fea 100644 --- a/composable-ui/src/hooks/use-cart.ts +++ b/composable-ui/src/hooks/use-cart.ts @@ -10,7 +10,7 @@ import { LOCAL_STORAGE_CART_ID, LOCAL_STORAGE_CART_UPDATED_AT, } from 'utils/constants' -import { Cart } from '@composable/types' +import { CartWithDiscounts as Cart } from '@composable/types' import { useSession } from 'next-auth/react' const USE_CART_KEY = 'useCartKey' diff --git a/composable-ui/src/server/intl/en-US.json b/composable-ui/src/server/intl/en-US.json index 651a240..100ef0f 100644 --- a/composable-ui/src/server/intl/en-US.json +++ b/composable-ui/src/server/intl/en-US.json @@ -117,6 +117,8 @@ "cart.summary.estimatedTotal": "Estimated Total", "cart.summary.orderTotal": "Order Total", + "cart.summary.totalDiscountAmount": "All discounts", + "cart.summary.grandPrice": "Grand Total", "cart.summary.shipping.complimentaryDelivery": "Complimentary Delivery", "cart.summary.shipping.free": "Free", "cart.summary.shipping": "Complimentary Delivery", diff --git a/packages/types/index.ts b/packages/types/index.ts index 801e302..d07f0bd 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -1,2 +1,3 @@ export * from './src/commerce' export * from './src/cms' +export * from './src/voucherify' diff --git a/packages/types/src/commerce/cart.ts b/packages/types/src/commerce/cart.ts index a29f42e..7e7804e 100644 --- a/packages/types/src/commerce/cart.ts +++ b/packages/types/src/commerce/cart.ts @@ -29,28 +29,3 @@ export interface CartItem { sku: string slug: string } - -// Extended cart related types by Voucherify discounts - -type CartItemWithDiscounts = CartItem & { - cartItemType: 'CartItemWithDiscounts' - discounts: { - subtotal_amount: string // Final order item amount after the applied item-level discount. If there are no item-level discounts applied - } -} - -type CartWithDiscounts = Cart & { - cartType: 'CartWithDiscounts' - redeemables: Redeemable[] - summary: { - discountAmount: string // Sum of all order-level discounts applied to the order. - totalDiscountAmount: string // Sum of all order-level AND all product-specific discounts applied to the order. - grandPrice: string // Order amount after applying all the discounts. - } -} - -type Redeemable = { - id: string - status: string - object: string -} diff --git a/packages/types/src/voucherify/cart-with-discounts.ts b/packages/types/src/voucherify/cart-with-discounts.ts new file mode 100644 index 0000000..f67c43d --- /dev/null +++ b/packages/types/src/voucherify/cart-with-discounts.ts @@ -0,0 +1,37 @@ +import { Cart, CartItem } from '../commerce' + +export type CartItemWithDiscounts = CartItem & { + cartItemType: 'CartItemWithDiscounts' + discounts: { + /** + * Final order item amount after the applied item-level discount. If there are no item-level discounts applied + */ + subtotalAmount: string + } +} + +export type CartWithDiscounts = Cart & { + cartType: 'CartWithDiscounts' + redeemables: Redeemable[] + items: CartItemWithDiscounts[] + summary: { + /** + * Sum of all order-level discounts applied to the order. + */ + discountAmount: string + /** + * Sum of all order-level AND all product-specific discounts applied to the order. + */ + totalDiscountAmount: string + /** + * Order amount after applying all the discounts. + */ + grandPrice: string + } +} + +export type Redeemable = { + id: string + status: string + object: string +} diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts new file mode 100644 index 0000000..1d65aa7 --- /dev/null +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -0,0 +1,18 @@ +import { CommerceService, Cart, CartItem } from '../commerce' +import { CartWithDiscounts } from './cart-with-discounts' + +export interface CommerceServiceWithDiscounts extends CommerceService { + addCartItem( + ...params: Parameters + ): Promise + createCart(): Promise + deleteCartItem( + ...params: Parameters + ): Promise + getCart( + ...params: Parameters + ): Promise + updateCartItem( + ...params: Parameters + ): Promise +} diff --git a/packages/types/src/voucherify/index.ts b/packages/types/src/voucherify/index.ts new file mode 100644 index 0000000..e0aca89 --- /dev/null +++ b/packages/types/src/voucherify/index.ts @@ -0,0 +1,2 @@ +export * from './cart-with-discounts' +export * from './commerce-service-with-discounts' diff --git a/packages/voucherify/src/index.ts b/packages/voucherify/src/index.ts index 4576d2f..d044e6d 100644 --- a/packages/voucherify/src/index.ts +++ b/packages/voucherify/src/index.ts @@ -1,31 +1,196 @@ -import { CommerceService } from '@composable/types' +import { + Cart, + CartItemWithDiscounts, + CartWithDiscounts, + CommerceService, + CommerceServiceWithDiscounts, + Redeemable, +} from '@composable/types' +import { + OrdersCreate, + ValidationValidateStackableResponse, + VoucherifyServerSide, +} from '@voucherify/sdk' -const hasKey = (obj: T, k: keyof any): k is keyof T => - k in obj +if ( + !process.env.VOUCHERIFY_APPLICATION_ID || + !process.env.VOUCHERIFY_SECRET_KEY || + !process.env.VOUCHERIFY_API_URL +) { + throw new Error('[voucherify] Missing configuration') +} + +const voucherify = VoucherifyServerSide({ + applicationId: process.env.VOUCHERIFY_APPLICATION_ID, + secretKey: process.env.VOUCHERIFY_SECRET_KEY, + exposeErrorCause: true, + apiUrl: process.env.VOUCHERIFY_API_URL, + channel: 'ComposableUI', +}) + +const toCent = (amount: string | undefined | null): number => { + if (!amount) { + return 0 + } + + return Math.round(parseFloat(amount) * 100) +} + +const centToString = (amount: number | null | undefined) => { + if (!amount) { + return '' + } + return Number(amount / 100).toString() +} + +const cartWithDiscount = ( + cart: Cart, + validationResponse: ValidationValidateStackableResponse | false +): CartWithDiscounts => { + console.log(validationResponse) + const redeemables: Redeemable[] = validationResponse + ? validationResponse.redeemables || [] + : [] // todo filter onlyr equired attributes + const items: CartItemWithDiscounts[] = cart.items.map((item) => ({ + ...item, + cartItemType: 'CartItemWithDiscounts', + discounts: { + subtotalAmount: '', // todo item level discounts + }, + })) + + const discountAmount = centToString( + validationResponse ? validationResponse.order?.discount_amount : 0 + ) + const grandPrice = centToString( + validationResponse + ? validationResponse.order?.total_amount + : toCent(cart.summary.totalPrice) + ) + const totalDiscountAmount = centToString( + validationResponse + ? validationResponse.order?.total_applied_discount_amount + : 0 + ) -export const commerceWithDiscount = (commerceService: CommerceService) => { - return commerceService + return { + ...cart, + cartType: 'CartWithDiscounts', + summary: { + ...cart.summary, + discountAmount, + totalDiscountAmount, + grandPrice, + }, + redeemables, + items, + } } -// export const commerceWithDiscount = (commerceService: CommerceService) => { -// return new Proxy(commerceService, { -// get: function (target, prop) { +const cartToVoucherifyOrder = (cart: Cart): OrdersCreate => { + return { + amount: toCent(cart.summary.totalPrice), + items: cart.items.map((item) => ({ + quantity: item.quantity, + product_id: item.id, + sku_id: item.sku, + price: item.price, + })), + } +} + +export const commerceWithDiscount = ( + commerceService: CommerceService +): CommerceServiceWithDiscounts => { + console.log('[voucherify] wrapping commerce service') + + const getCart = async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.getCart(...props) -// if(prop === 'getCart'){ -// return async (...props: Parameters) => { -// console.log('get cart items params', props) + if (!cart) { + return cart + } -// const cart = await target.getCart(...props) + const validationResponse = await voucherify.validations.validateStackable({ + redeemables: [{ object: 'voucher', id: '10%OFF' }], + order: cartToVoucherifyOrder(cart), + }) -// console.log('result', cart) + return cartWithDiscount(cart, validationResponse) + } -// return new Promise(resolve => cart) -// } -// } + const addCartItem = async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.addCartItem(...props) + if (!cart) { + return cart + } -// return hasKey(target, prop) ? target[prop] : () => { -// throw new Error('Function deos not exists') -// }; -// } -// }); -// } + const validationResponse = await voucherify.validations.validateStackable({ + redeemables: [{ object: 'voucher', id: '10%OFF' }], + order: cartToVoucherifyOrder(cart), + }) + + return cartWithDiscount(cart, validationResponse) + } + + const createCart = async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.createCart(...props) + if (!cart) { + return cart + } + + const validationResponse = await voucherify.validations.validateStackable({ + redeemables: [{ object: 'voucher', id: '10%OFF' }], + order: cartToVoucherifyOrder(cart), + }) + + return cartWithDiscount(cart, validationResponse) + } + + const deleteCartItem = async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.deleteCartItem(...props) + if (!cart) { + return cart + } + + const validationResponse = await voucherify.validations.validateStackable({ + redeemables: [{ object: 'voucher', id: '10%OFF' }], + order: cartToVoucherifyOrder(cart), + }) + + return cartWithDiscount(cart, validationResponse) + } + + const updateCartItem = async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.updateCartItem(...props) + if (!cart) { + return cart + } + + const validationResponse = await voucherify.validations.validateStackable({ + redeemables: [{ object: 'voucher', id: '10%OFF' }], + order: cartToVoucherifyOrder(cart), + }) + + return cartWithDiscount(cart, validationResponse) + } + + return { + ...commerceService, + getCart, + addCartItem, + createCart, + deleteCartItem, + updateCartItem, + } +} From 5ca245b955492c94fad23734357c0f0f000fc8a6 Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Wed, 18 Oct 2023 13:40:20 +0200 Subject: [PATCH 03/13] PoC coupon on frontend side --- .../src/components/cart/cart-summary.tsx | 3 +- .../src/components/forms/coupon-form.tsx | 149 +++++++++++ composable-ui/src/hooks/use-cart.ts | 124 ++++++++- .../commerce/procedures/cart/add-coupon.ts | 14 + .../commerce/procedures/cart/delete-coupon.ts | 14 + .../routers/commerce/procedures/cart/index.ts | 2 + composable-ui/src/server/intl/en-US.json | 2 + .../commerce-service-with-discounts.ts | 13 + .../src/cart-to-voucherify-order.ts | 15 ++ packages/voucherify/src/cart-with-discount.ts | 51 ++++ packages/voucherify/src/index.ts | 240 +++++++++++------- packages/voucherify/src/to-cent.ts | 14 + 12 files changed, 545 insertions(+), 96 deletions(-) create mode 100644 composable-ui/src/components/forms/coupon-form.tsx create mode 100644 composable-ui/src/server/api/routers/commerce/procedures/cart/add-coupon.ts create mode 100644 composable-ui/src/server/api/routers/commerce/procedures/cart/delete-coupon.ts create mode 100644 packages/voucherify/src/cart-to-voucherify-order.ts create mode 100644 packages/voucherify/src/cart-with-discount.ts create mode 100644 packages/voucherify/src/to-cent.ts diff --git a/composable-ui/src/components/cart/cart-summary.tsx b/composable-ui/src/components/cart/cart-summary.tsx index ac4809a..8a243f7 100644 --- a/composable-ui/src/components/cart/cart-summary.tsx +++ b/composable-ui/src/components/cart/cart-summary.tsx @@ -2,6 +2,7 @@ import { useIntl } from 'react-intl' import { useRouter } from 'next/router' import { CartData, useCart } from 'hooks' import { Price } from 'components/price' +import { CouponForm } from 'components/forms/coupon-form' import { Box, Button, @@ -90,7 +91,7 @@ export const CartSummary = ({ )} - + {_cartData.summary?.totalDiscountAmount && ( > + signIn?: typeof signIn + type?: AccountPage +} + +export const CouponForm = ({ + signIn, + type = AccountPage.PAGE, + setAccountFormToShow, +}: LoginFormProps) => { + const { cart, addCartCoupon, deleteCartCoupon } = useCart() + const intl = useIntl() + const { data, status } = useSession() + const [simulatingLoading, setSimulatingLoading] = useState(false) + const [isError, setIsError] = useState(false) + const { + register, + handleSubmit, + formState: { errors }, + } = useForm<{ email: string; password: string }>({ + resolver: yupResolver(loginFormSchema({ intl })), + mode: 'all', + }) + + const content = { + ariaLabel: { + signIn: intl.formatMessage({ id: 'account.login.title' }), + }, + title: intl.formatMessage({ id: 'account.login.title' }), + description: intl.formatMessage({ id: 'account.login.description' }), + loginWithFacebook: intl.formatMessage({ + id: 'account.login.loginWithFacebook', + }), + loginWithGoogle: intl.formatMessage({ + id: 'account.login.loginWithGoogle', + }), + notAMemberYet: intl.formatMessage({ id: 'account.login.notAMemberYet' }), + createAnAccount: intl.formatMessage({ + id: 'account.login.createAnAccount', + }), + or: intl.formatMessage({ id: 'text.or' }), + input: { + coupon: { + label: intl.formatMessage({ id: 'cart.summary.label.coupon' }), + placeholder: intl.formatMessage({ id: 'cart.summary.label.coupon' }), + }, + }, + button: { + login: intl.formatMessage({ id: 'action.addCoupon' }), + }, + error: { + incorrectSignIn: intl.formatMessage({ + id: 'account.login.error.incorrectSignIn', + }), + }, + } + + return ( + + {isError && ( + + + {content.error.incorrectSignIn} + + )} +
{ + setIsError(false) + setSimulatingLoading(true) + + addCartCoupon.mutate({ cartId: cart.id || '', coupon: data.email }) + })} + > + + + + } + type="submit" + size="sm" + variant={'outline'} + /> + +
+ + {cart.redeemables?.map((redeemable) => ( + + {redeemable.id} + + deleteCartCoupon.mutate({ + cartId: cart.id || '', + coupon: redeemable.id, + }) + } + /> + + ))} + + {/*
{JSON.stringify(cart, null,2)}
*/} +
+ ) +} + +const loginFormSchema = (deps: { intl: IntlShape }) => { + const { intl } = deps + return yup.object().shape({ + email: yup + .string() + .required(intl.formatMessage({ id: 'validation.emailRequired' })), + }) +} diff --git a/composable-ui/src/hooks/use-cart.ts b/composable-ui/src/hooks/use-cart.ts index 6077fea..ee877e7 100644 --- a/composable-ui/src/hooks/use-cart.ts +++ b/composable-ui/src/hooks/use-cart.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { deleteFromStorage, @@ -10,12 +10,12 @@ import { LOCAL_STORAGE_CART_ID, LOCAL_STORAGE_CART_UPDATED_AT, } from 'utils/constants' -import { CartWithDiscounts as Cart } from '@composable/types' +import { CartWithDiscounts } from '@composable/types' import { useSession } from 'next-auth/react' const USE_CART_KEY = 'useCartKey' -export type CartData = Partial & { +export type CartData = Partial & { isLoading: boolean isEmpty: boolean quantity: number @@ -35,9 +35,13 @@ const setCartId = (id: string) => { interface UseCartOptions { onCartItemAddError?: () => void + onCartCouponAddError?: () => void + onCartCouponDeleteError?: () => void onCartItemUpdateError?: () => void onCartItemDeleteError?: () => void - onCartItemAddSuccess?: (cart: Cart) => void + onCartCouponAddSuccess?: (response: { cart: CartWithDiscounts }) => void + onCartCouponDeleteSuccess?: (cart: CartWithDiscounts) => void + onCartItemAddSuccess?: (cart: CartWithDiscounts) => void } export const useCart = (options?: UseCartOptions) => { @@ -227,6 +231,100 @@ export const useCart = (options?: UseCartOptions) => { [cartId, cartItemDelete] ) + /** + * Cart Coupon Add + */ + const cartCouponAdd = useMutation( + ['cartCouponAdd'], + async (variables: { cartId: string; coupon: string }) => { + const params = { + cartId: variables.cartId, + coupon: variables.coupon, + } + + const response = await client.commerce.addCoupon.mutate(params) + const updatedAt = Date.now() + console.log('asdasd', { response }) + queryClient.setQueryData( + [USE_CART_KEY, variables.cartId, updatedAt], + response.cart + ) + + setCartUpdatedAt(updatedAt) + + return response + }, + { + onError: optionsRef.current?.onCartCouponAddError, + } + ) + + /** + * Cart Coupon Add Mutation + */ + const cartCouponAddMutation = useCallback( + async (params: { cartId: string; coupon: string }) => { + const id = cartId ? cartId : await cartCreate.mutateAsync() + await cartCouponAdd.mutate( + { + cartId: id, + coupon: params.coupon, + }, + { + onSuccess: optionsRef.current?.onCartCouponAddSuccess, + } + ) + }, + [cartId, cartCreate, cartCouponAdd] + ) + + /** + * Cart Coupon Delete + */ + const cartCouponDelete = useMutation( + ['cartCouponAdd'], + async (variables: { cartId: string; coupon: string }) => { + const params = { + cartId: variables.cartId, + coupon: variables.coupon, + } + + const response = await client.commerce.deleteCoupon.mutate(params) + const updatedAt = Date.now() + + queryClient.setQueryData( + [USE_CART_KEY, variables.cartId, updatedAt], + response + ) + + setCartUpdatedAt(updatedAt) + + return response + }, + { + onError: optionsRef.current?.onCartCouponDeleteError, + } + ) + + /** + * Cart Coupon Delete Mutation + */ + const cartCouponDeleteMutation = useCallback( + async (params: { cartId: string; coupon: string }) => { + const id = cartId ? cartId : await cartCreate.mutateAsync() + await cartCouponDelete.mutate( + { + cartId: id, + coupon: params.coupon, + }, + { + onSuccess: optionsRef.current?.onCartCouponDeleteSuccess, + } + ) + }, + [cartId, cartCreate, cartCouponDelete] + ) + /** * Cart Item Add Facade */ @@ -235,6 +333,22 @@ export const useCart = (options?: UseCartOptions) => { isLoading: cartItemAdd.isLoading || cartCreate.isLoading, } + /** + * Cart Coupon Add Facade + */ + const addCartCoupon = { + mutate: cartCouponAddMutation, + isLoading: cartCouponAdd.isLoading || cartCreate.isLoading, + } + + /** + * Cart Coupon Delete Facade + */ + const deleteCartCoupon = { + mutate: cartCouponDeleteMutation, + isLoading: cartCouponDelete.isLoading || cartCreate.isLoading, + } + /** * Cart Item Update Facade */ @@ -278,6 +392,8 @@ export const useCart = (options?: UseCartOptions) => { */ return { addCartItem, + addCartCoupon, + deleteCartCoupon, updateCartItem, deleteCartItem, cart: cartData, diff --git a/composable-ui/src/server/api/routers/commerce/procedures/cart/add-coupon.ts b/composable-ui/src/server/api/routers/commerce/procedures/cart/add-coupon.ts new file mode 100644 index 0000000..eb0bf2f --- /dev/null +++ b/composable-ui/src/server/api/routers/commerce/procedures/cart/add-coupon.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' +import { protectedProcedure } from 'server/api/trpc' +import { commerce } from 'server/data-source' + +export const addCoupon = protectedProcedure + .input( + z.object({ + cartId: z.string(), + coupon: z.string(), + }) + ) + .mutation(async ({ input }) => { + return await commerce.addCoupon({ ...input }) + }) diff --git a/composable-ui/src/server/api/routers/commerce/procedures/cart/delete-coupon.ts b/composable-ui/src/server/api/routers/commerce/procedures/cart/delete-coupon.ts new file mode 100644 index 0000000..48afc09 --- /dev/null +++ b/composable-ui/src/server/api/routers/commerce/procedures/cart/delete-coupon.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' +import { protectedProcedure } from 'server/api/trpc' +import { commerce } from 'server/data-source' + +export const deleteCoupon = protectedProcedure + .input( + z.object({ + cartId: z.string(), + coupon: z.string(), + }) + ) + .mutation(async ({ input }) => { + return await commerce.deleteCoupon({ ...input }) + }) diff --git a/composable-ui/src/server/api/routers/commerce/procedures/cart/index.ts b/composable-ui/src/server/api/routers/commerce/procedures/cart/index.ts index c00875d..eb4329b 100644 --- a/composable-ui/src/server/api/routers/commerce/procedures/cart/index.ts +++ b/composable-ui/src/server/api/routers/commerce/procedures/cart/index.ts @@ -3,3 +3,5 @@ export * from './create-cart' export * from './delete-cart-item' export * from './get-cart' export * from './update-cart-item' +export * from './add-coupon' +export * from './delete-coupon' diff --git a/composable-ui/src/server/intl/en-US.json b/composable-ui/src/server/intl/en-US.json index 100ef0f..645da70 100644 --- a/composable-ui/src/server/intl/en-US.json +++ b/composable-ui/src/server/intl/en-US.json @@ -85,6 +85,7 @@ "action.selectCountry": "Select Country", "action.send": "Send", "action.signIn": "Sign In", + "action.addCoupon": "Add Coupon", "action.signOut": "Log Out", "action.signup": "Sign Up", "action.startShopping": "Start Shopping", @@ -127,6 +128,7 @@ "cart.summary.taxes": "Taxes", "cart.summary.title": "Order Summary", "cart.summary.total": "Total", + "cart.summary.label.coupon": "Coupon code", "checkout.title": "Checkout", diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts index 1d65aa7..48bf6c4 100644 --- a/packages/types/src/voucherify/commerce-service-with-discounts.ts +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -2,6 +2,8 @@ import { CommerceService, Cart, CartItem } from '../commerce' import { CartWithDiscounts } from './cart-with-discounts' export interface CommerceServiceWithDiscounts extends CommerceService { + // Extend exisiting commerce service methods to return cart with applied discount detasils + addCartItem( ...params: Parameters ): Promise @@ -15,4 +17,15 @@ export interface CommerceServiceWithDiscounts extends CommerceService { updateCartItem( ...params: Parameters ): Promise + + // Additional commerce endpoints to manage applied coupons + + addCoupon(props: { + coupon: string + cartId: string + }): Promise<{ cart: CartWithDiscounts; result: boolean; errorMsg?: string }> + deleteCoupon(props: { + coupon: string + cartId: string + }): Promise } diff --git a/packages/voucherify/src/cart-to-voucherify-order.ts b/packages/voucherify/src/cart-to-voucherify-order.ts new file mode 100644 index 0000000..7715635 --- /dev/null +++ b/packages/voucherify/src/cart-to-voucherify-order.ts @@ -0,0 +1,15 @@ +import { Cart } from '@composable/types' +import { OrdersCreate } from '@voucherify/sdk' +import { toCent } from './to-cent' + +export const cartToVoucherifyOrder = (cart: Cart): OrdersCreate => { + return { + amount: toCent(cart.summary.totalPrice), + items: cart.items.map((item) => ({ + quantity: item.quantity, + product_id: item.id, + sku_id: item.sku, + price: item.price, + })), + } +} diff --git a/packages/voucherify/src/cart-with-discount.ts b/packages/voucherify/src/cart-with-discount.ts new file mode 100644 index 0000000..291a07c --- /dev/null +++ b/packages/voucherify/src/cart-with-discount.ts @@ -0,0 +1,51 @@ +import { + Cart, + CartItemWithDiscounts, + CartWithDiscounts, + Redeemable, +} from '@composable/types' +import { ValidationValidateStackableResponse } from '@voucherify/sdk' +import { centToString, toCent } from './to-cent' + +export const cartWithDiscount = ( + cart: Cart, + validationResponse: ValidationValidateStackableResponse | false +): CartWithDiscounts => { + const redeemables: Redeemable[] = validationResponse + ? validationResponse.redeemables || [] + : [] // todo filter onlyr equired attributes + const items: CartItemWithDiscounts[] = cart.items.map((item) => ({ + ...item, + cartItemType: 'CartItemWithDiscounts', + discounts: { + subtotalAmount: '', // todo item level discounts + }, + })) + + const discountAmount = centToString( + validationResponse ? validationResponse.order?.discount_amount : 0 + ) + const grandPrice = centToString( + validationResponse + ? validationResponse.order?.total_amount + : toCent(cart.summary.totalPrice) + ) + const totalDiscountAmount = centToString( + validationResponse + ? validationResponse.order?.total_applied_discount_amount + : 0 + ) + + return { + ...cart, + cartType: 'CartWithDiscounts', + summary: { + ...cart.summary, + discountAmount, + totalDiscountAmount, + grandPrice, + }, + redeemables, + items, + } +} diff --git a/packages/voucherify/src/index.ts b/packages/voucherify/src/index.ts index d044e6d..113f05c 100644 --- a/packages/voucherify/src/index.ts +++ b/packages/voucherify/src/index.ts @@ -1,16 +1,15 @@ import { Cart, - CartItemWithDiscounts, CartWithDiscounts, CommerceService, CommerceServiceWithDiscounts, - Redeemable, } from '@composable/types' import { - OrdersCreate, ValidationValidateStackableResponse, VoucherifyServerSide, } from '@voucherify/sdk' +import { cartWithDiscount } from './cart-with-discount' +import { cartToVoucherifyOrder } from './cart-to-voucherify-order' if ( !process.env.VOUCHERIFY_APPLICATION_ID || @@ -28,81 +27,37 @@ const voucherify = VoucherifyServerSide({ channel: 'ComposableUI', }) -const toCent = (amount: string | undefined | null): number => { - if (!amount) { - return 0 +type Redeemable = { + object: 'voucher' + id: string + status: 'INAPPLICABLE' | 'APPLICABLE' | 'SKIPPED' | 'NEW' + result?: { + error?: { key: string; message: string; details: string } } - - return Math.round(parseFloat(amount) * 100) } -const centToString = (amount: number | null | undefined) => { - if (!amount) { - return '' - } - return Number(amount / 100).toString() +type CartDiscountsStorage = { + [cartId: string]: Redeemable[] } -const cartWithDiscount = ( - cart: Cart, - validationResponse: ValidationValidateStackableResponse | false -): CartWithDiscounts => { - console.log(validationResponse) - const redeemables: Redeemable[] = validationResponse - ? validationResponse.redeemables || [] - : [] // todo filter onlyr equired attributes - const items: CartItemWithDiscounts[] = cart.items.map((item) => ({ - ...item, - cartItemType: 'CartItemWithDiscounts', - discounts: { - subtotalAmount: '', // todo item level discounts - }, - })) - - const discountAmount = centToString( - validationResponse ? validationResponse.order?.discount_amount : 0 - ) - const grandPrice = centToString( - validationResponse - ? validationResponse.order?.total_amount - : toCent(cart.summary.totalPrice) - ) - const totalDiscountAmount = centToString( - validationResponse - ? validationResponse.order?.total_applied_discount_amount - : 0 - ) - - return { - ...cart, - cartType: 'CartWithDiscounts', - summary: { - ...cart.summary, - discountAmount, - totalDiscountAmount, - grandPrice, - }, - redeemables, - items, - } +/** + * In memory storage that presist coupons for specific cart + */ +const cartDiscountsStorage: CartDiscountsStorage = { + '7a6dd462-24dc-11ed-861d-0242ac120002': [ + { object: 'voucher', id: '10%OFF', status: 'NEW' }, + ], // example cart discount } -const cartToVoucherifyOrder = (cart: Cart): OrdersCreate => { - return { - amount: toCent(cart.summary.totalPrice), - items: cart.items.map((item) => ({ - quantity: item.quantity, - product_id: item.id, - sku_id: item.sku, - price: item.price, - })), - } +const hasAtLeastOneRedeemable = (cartId: string) => { + console.log({ cartId, cartDiscountsStorage }) + return cartDiscountsStorage[cartId] && cartDiscountsStorage[cartId].length } export const commerceWithDiscount = ( commerceService: CommerceService ): CommerceServiceWithDiscounts => { - console.log('[voucherify] wrapping commerce service') + console.log('[voucherify] wrapping commerce service', cartDiscountsStorage) const getCart = async ( ...props: Parameters @@ -113,10 +68,12 @@ export const commerceWithDiscount = ( return cart } - const validationResponse = await voucherify.validations.validateStackable({ - redeemables: [{ object: 'voucher', id: '10%OFF' }], - order: cartToVoucherifyOrder(cart), - }) + const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) + ? await voucherify.validations.validateStackable({ + redeemables: cartDiscountsStorage[props[0].cartId] || [], + order: cartToVoucherifyOrder(cart), + }) + : false return cartWithDiscount(cart, validationResponse) } @@ -129,10 +86,12 @@ export const commerceWithDiscount = ( return cart } - const validationResponse = await voucherify.validations.validateStackable({ - redeemables: [{ object: 'voucher', id: '10%OFF' }], - order: cartToVoucherifyOrder(cart), - }) + const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) + ? await voucherify.validations.validateStackable({ + redeemables: cartDiscountsStorage[props[0].cartId] || [], + order: cartToVoucherifyOrder(cart), + }) + : false return cartWithDiscount(cart, validationResponse) } @@ -141,16 +100,8 @@ export const commerceWithDiscount = ( ...props: Parameters ): Promise => { const cart = await commerceService.createCart(...props) - if (!cart) { - return cart - } - - const validationResponse = await voucherify.validations.validateStackable({ - redeemables: [{ object: 'voucher', id: '10%OFF' }], - order: cartToVoucherifyOrder(cart), - }) - return cartWithDiscount(cart, validationResponse) + return cartWithDiscount(cart, false) } const deleteCartItem = async ( @@ -161,10 +112,12 @@ export const commerceWithDiscount = ( return cart } - const validationResponse = await voucherify.validations.validateStackable({ - redeemables: [{ object: 'voucher', id: '10%OFF' }], - order: cartToVoucherifyOrder(cart), - }) + const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) + ? await voucherify.validations.validateStackable({ + redeemables: cartDiscountsStorage[props[0].cartId] || [], + order: cartToVoucherifyOrder(cart), + }) + : false return cartWithDiscount(cart, validationResponse) } @@ -177,10 +130,113 @@ export const commerceWithDiscount = ( return cart } - const validationResponse = await voucherify.validations.validateStackable({ - redeemables: [{ object: 'voucher', id: '10%OFF' }], - order: cartToVoucherifyOrder(cart), - }) + const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) + ? await voucherify.validations.validateStackable({ + redeemables: cartDiscountsStorage[props[0].cartId] || [], + order: cartToVoucherifyOrder(cart), + }) + : false + + return cartWithDiscount(cart, validationResponse) + } + + const addCoupon = async ({ + cartId, + coupon, + }: { + cartId: string + coupon: string + }) => { + let errorMsg: string | undefined + + if (!cartDiscountsStorage[cartId]) { + cartDiscountsStorage[cartId] = [ + { object: 'voucher', id: coupon, status: 'NEW' }, + ] + } else { + cartDiscountsStorage[cartId].push({ + object: 'voucher', + id: coupon, + status: 'NEW', + }) + } + + const cart = await commerceService.getCart({ cartId }) + + if (!cart) { + throw new Error('[voucherify][addCoupon ] cart not found') + } + + console.log( + `[voucherify][addCoupon] Add coupon ${coupon} to cart ${cartId}` + ) + + const validationResponse = hasAtLeastOneRedeemable(cartId) + ? await voucherify.validations.validateStackable({ + redeemables: cartDiscountsStorage[cartId], + order: cartToVoucherifyOrder(cart), + }) + : false + + console.log(`[voucherify][addCoupon] valiadtion result`, validationResponse) + + //@ts-ignore + const addedRedeembale = + validationResponse && + validationResponse.redeemables && + validationResponse.inapplicable_redeemables + ? [ + ...validationResponse.redeemables, + ...validationResponse?.inapplicable_redeemables, + ]?.find((redeemable) => redeemable.id === coupon) + : false + const result = addedRedeembale + ? addedRedeembale.status === 'APPLICABLE' + : false + + if (!result) { + errorMsg = addedRedeembale + ? addedRedeembale.result?.error?.message + : 'Redeemable not found in response from Voucherify' + } + + return { + cart: cartWithDiscount(cart, validationResponse), + result, + errorMsg, + } + } + + const deleteCoupon = async ({ + cartId, + coupon, + }: { + cartId: string + coupon: string + }) => { + let errorMsg: string | undefined + + if (cartDiscountsStorage[cartId]) { + cartDiscountsStorage[cartId] = cartDiscountsStorage[cartId].filter( + (redeemable) => redeemable.id !== coupon + ) + } + const cart = await commerceService.getCart({ cartId }) + + if (!cart) { + throw new Error('[voucherify][deleteCoupon] cart not found') + } + + console.log( + `[voucherify][deleteCoupon] Delete coupon ${coupon} from cart ${cartId}` + ) + + const validationResponse = hasAtLeastOneRedeemable(cartId) + ? await voucherify.validations.validateStackable({ + redeemables: cartDiscountsStorage[cartId], + order: cartToVoucherifyOrder(cart), + }) + : false return cartWithDiscount(cart, validationResponse) } @@ -192,5 +248,7 @@ export const commerceWithDiscount = ( createCart, deleteCartItem, updateCartItem, + addCoupon, + deleteCoupon, } } diff --git a/packages/voucherify/src/to-cent.ts b/packages/voucherify/src/to-cent.ts new file mode 100644 index 0000000..1667a66 --- /dev/null +++ b/packages/voucherify/src/to-cent.ts @@ -0,0 +1,14 @@ +export const toCent = (amount: string | undefined | null): number => { + if (!amount) { + return 0 + } + + return Math.round(parseFloat(amount) * 100) +} + +export const centToString = (amount: number | null | undefined) => { + if (!amount) { + return '' + } + return Number(amount / 100).toString() +} From be8b4e4cfc867ca86a7f0b3bd7be0a9c37593054 Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Wed, 18 Oct 2023 16:31:44 +0200 Subject: [PATCH 04/13] PoC handling coupon errors on frontend --- .../src/components/forms/coupon-form.tsx | 59 +++++--------- composable-ui/src/hooks/use-cart.ts | 9 ++- packages/voucherify/src/index.ts | 81 ++++++++++--------- 3 files changed, 70 insertions(+), 79 deletions(-) diff --git a/composable-ui/src/components/forms/coupon-form.tsx b/composable-ui/src/components/forms/coupon-form.tsx index 675bf2f..f73bdbd 100644 --- a/composable-ui/src/components/forms/coupon-form.tsx +++ b/composable-ui/src/components/forms/coupon-form.tsx @@ -23,37 +23,25 @@ export const CouponForm = ({ type = AccountPage.PAGE, setAccountFormToShow, }: LoginFormProps) => { - const { cart, addCartCoupon, deleteCartCoupon } = useCart() const intl = useIntl() - const { data, status } = useSession() - const [simulatingLoading, setSimulatingLoading] = useState(false) const [isError, setIsError] = useState(false) const { register, handleSubmit, + setError, + setValue, formState: { errors }, - } = useForm<{ email: string; password: string }>({ - resolver: yupResolver(loginFormSchema({ intl })), + } = useForm<{ coupon: string }>({ + resolver: yupResolver(couponFormSchema()), mode: 'all', }) + const { cart, addCartCoupon, deleteCartCoupon } = useCart({ + onCartCouponAddError: (msg) => { + setError('coupon', { message: msg || 'Could not add coupon' }) + }, + }) const content = { - ariaLabel: { - signIn: intl.formatMessage({ id: 'account.login.title' }), - }, - title: intl.formatMessage({ id: 'account.login.title' }), - description: intl.formatMessage({ id: 'account.login.description' }), - loginWithFacebook: intl.formatMessage({ - id: 'account.login.loginWithFacebook', - }), - loginWithGoogle: intl.formatMessage({ - id: 'account.login.loginWithGoogle', - }), - notAMemberYet: intl.formatMessage({ id: 'account.login.notAMemberYet' }), - createAnAccount: intl.formatMessage({ - id: 'account.login.createAnAccount', - }), - or: intl.formatMessage({ id: 'text.or' }), input: { coupon: { label: intl.formatMessage({ id: 'cart.summary.label.coupon' }), @@ -63,11 +51,6 @@ export const CouponForm = ({ button: { login: intl.formatMessage({ id: 'action.addCoupon' }), }, - error: { - incorrectSignIn: intl.formatMessage({ - id: 'account.login.error.incorrectSignIn', - }), - }, } return ( @@ -75,17 +58,19 @@ export const CouponForm = ({ {isError && ( - {content.error.incorrectSignIn} + asdasdasd )}
{ setIsError(false) - setSimulatingLoading(true) - - addCartCoupon.mutate({ cartId: cart.id || '', coupon: data.email }) + setValue('coupon', '') + // setError('coupon', {message: 'Could not add coupon' }) + await addCartCoupon.mutate({ + cartId: cart.id || '', + coupon: data.coupon, + }) })} > @@ -134,16 +119,12 @@ export const CouponForm = ({ ))} - {/*
{JSON.stringify(cart, null,2)}
*/}
) } -const loginFormSchema = (deps: { intl: IntlShape }) => { - const { intl } = deps +const couponFormSchema = () => { return yup.object().shape({ - email: yup - .string() - .required(intl.formatMessage({ id: 'validation.emailRequired' })), + coupon: yup.string().required(), }) } diff --git a/composable-ui/src/hooks/use-cart.ts b/composable-ui/src/hooks/use-cart.ts index ee877e7..825f773 100644 --- a/composable-ui/src/hooks/use-cart.ts +++ b/composable-ui/src/hooks/use-cart.ts @@ -35,7 +35,7 @@ const setCartId = (id: string) => { interface UseCartOptions { onCartItemAddError?: () => void - onCartCouponAddError?: () => void + onCartCouponAddError?: (errorMessage: string) => void onCartCouponDeleteError?: () => void onCartItemUpdateError?: () => void onCartItemDeleteError?: () => void @@ -244,7 +244,6 @@ export const useCart = (options?: UseCartOptions) => { const response = await client.commerce.addCoupon.mutate(params) const updatedAt = Date.now() - console.log('asdasd', { response }) queryClient.setQueryData( [USE_CART_KEY, variables.cartId, updatedAt], response.cart @@ -252,6 +251,12 @@ export const useCart = (options?: UseCartOptions) => { setCartUpdatedAt(updatedAt) + if (!response.result && optionsRef.current?.onCartCouponAddError) { + optionsRef.current?.onCartCouponAddError( + response.errorMsg || `Could not add ${variables.coupon} coupon` + ) + } + return response }, { diff --git a/packages/voucherify/src/index.ts b/packages/voucherify/src/index.ts index 113f05c..31e5f51 100644 --- a/packages/voucherify/src/index.ts +++ b/packages/voucherify/src/index.ts @@ -5,6 +5,8 @@ import { CommerceServiceWithDiscounts, } from '@composable/types' import { + ApplicableToResultList, + StackableRedeemableResponse, ValidationValidateStackableResponse, VoucherifyServerSide, } from '@voucherify/sdk' @@ -27,26 +29,15 @@ const voucherify = VoucherifyServerSide({ channel: 'ComposableUI', }) -type Redeemable = { - object: 'voucher' - id: string - status: 'INAPPLICABLE' | 'APPLICABLE' | 'SKIPPED' | 'NEW' - result?: { - error?: { key: string; message: string; details: string } - } -} - type CartDiscountsStorage = { - [cartId: string]: Redeemable[] + [cartId: string]: string[] } /** * In memory storage that presist coupons for specific cart */ const cartDiscountsStorage: CartDiscountsStorage = { - '7a6dd462-24dc-11ed-861d-0242ac120002': [ - { object: 'voucher', id: '10%OFF', status: 'NEW' }, - ], // example cart discount + '7a6dd462-24dc-11ed-861d-0242ac120002': ['10%OFF'], // example cart discount } const hasAtLeastOneRedeemable = (cartId: string) => { @@ -54,6 +45,12 @@ const hasAtLeastOneRedeemable = (cartId: string) => { return cartDiscountsStorage[cartId] && cartDiscountsStorage[cartId].length } +const getRedeemmablesForValidation = (couponCodes: string[]) => + couponCodes.map((couponCode) => ({ + id: couponCode, + object: 'voucher' as const, + })) + export const commerceWithDiscount = ( commerceService: CommerceService ): CommerceServiceWithDiscounts => { @@ -70,7 +67,9 @@ export const commerceWithDiscount = ( const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) ? await voucherify.validations.validateStackable({ - redeemables: cartDiscountsStorage[props[0].cartId] || [], + redeemables: getRedeemmablesForValidation( + cartDiscountsStorage[props[0].cartId] + ), order: cartToVoucherifyOrder(cart), }) : false @@ -88,7 +87,9 @@ export const commerceWithDiscount = ( const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) ? await voucherify.validations.validateStackable({ - redeemables: cartDiscountsStorage[props[0].cartId] || [], + redeemables: getRedeemmablesForValidation( + cartDiscountsStorage[props[0].cartId] + ), order: cartToVoucherifyOrder(cart), }) : false @@ -114,7 +115,9 @@ export const commerceWithDiscount = ( const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) ? await voucherify.validations.validateStackable({ - redeemables: cartDiscountsStorage[props[0].cartId] || [], + redeemables: getRedeemmablesForValidation( + cartDiscountsStorage[props[0].cartId] + ), order: cartToVoucherifyOrder(cart), }) : false @@ -132,7 +135,9 @@ export const commerceWithDiscount = ( const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) ? await voucherify.validations.validateStackable({ - redeemables: cartDiscountsStorage[props[0].cartId] || [], + redeemables: getRedeemmablesForValidation( + cartDiscountsStorage[props[0].cartId] + ), order: cartToVoucherifyOrder(cart), }) : false @@ -150,15 +155,7 @@ export const commerceWithDiscount = ( let errorMsg: string | undefined if (!cartDiscountsStorage[cartId]) { - cartDiscountsStorage[cartId] = [ - { object: 'voucher', id: coupon, status: 'NEW' }, - ] - } else { - cartDiscountsStorage[cartId].push({ - object: 'voucher', - id: coupon, - status: 'NEW', - }) + cartDiscountsStorage[cartId] = [] } const cart = await commerceService.getCart({ cartId }) @@ -171,30 +168,38 @@ export const commerceWithDiscount = ( `[voucherify][addCoupon] Add coupon ${coupon} to cart ${cartId}` ) - const validationResponse = hasAtLeastOneRedeemable(cartId) + const validationResponse: + | false + | (ValidationValidateStackableResponse & { + inapplicable_redeemables?: StackableRedeemableResponse[] + }) = hasAtLeastOneRedeemable(cartId) ? await voucherify.validations.validateStackable({ - redeemables: cartDiscountsStorage[cartId], + redeemables: getRedeemmablesForValidation([ + ...cartDiscountsStorage[cartId], + coupon, + ]), order: cartToVoucherifyOrder(cart), }) : false console.log(`[voucherify][addCoupon] valiadtion result`, validationResponse) - //@ts-ignore const addedRedeembale = - validationResponse && - validationResponse.redeemables && - validationResponse.inapplicable_redeemables + validationResponse && validationResponse.redeemables ? [ ...validationResponse.redeemables, - ...validationResponse?.inapplicable_redeemables, + ...(validationResponse?.inapplicable_redeemables || []), ]?.find((redeemable) => redeemable.id === coupon) : false + const result = addedRedeembale ? addedRedeembale.status === 'APPLICABLE' : false - if (!result) { + console.log({ result }) + if (result) { + cartDiscountsStorage[cartId].push(coupon) + } else { errorMsg = addedRedeembale ? addedRedeembale.result?.error?.message : 'Redeemable not found in response from Voucherify' @@ -214,11 +219,9 @@ export const commerceWithDiscount = ( cartId: string coupon: string }) => { - let errorMsg: string | undefined - if (cartDiscountsStorage[cartId]) { cartDiscountsStorage[cartId] = cartDiscountsStorage[cartId].filter( - (redeemable) => redeemable.id !== coupon + (redeemable) => redeemable !== coupon ) } const cart = await commerceService.getCart({ cartId }) @@ -233,7 +236,9 @@ export const commerceWithDiscount = ( const validationResponse = hasAtLeastOneRedeemable(cartId) ? await voucherify.validations.validateStackable({ - redeemables: cartDiscountsStorage[cartId], + redeemables: getRedeemmablesForValidation( + cartDiscountsStorage[cartId] + ), order: cartToVoucherifyOrder(cart), }) : false From 0f512b6f1f35bde61c60faea3cf08dc4f2c6f103 Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Wed, 25 Oct 2023 13:47:34 +0200 Subject: [PATCH 05/13] refactoring --- .../src/components/forms/coupon-form.tsx | 3 +- packages/voucherify/src/index.ts | 28 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/composable-ui/src/components/forms/coupon-form.tsx b/composable-ui/src/components/forms/coupon-form.tsx index f73bdbd..5747768 100644 --- a/composable-ui/src/components/forms/coupon-form.tsx +++ b/composable-ui/src/components/forms/coupon-form.tsx @@ -65,12 +65,13 @@ export const CouponForm = ({ role={'form'} onSubmit={handleSubmit(async (data) => { setIsError(false) - setValue('coupon', '') + // setError('coupon', {message: 'Could not add coupon' }) await addCartCoupon.mutate({ cartId: cart.id || '', coupon: data.coupon, }) + setValue('coupon', '') })} > Date: Tue, 31 Oct 2023 10:02:19 +0100 Subject: [PATCH 06/13] reafactoring round one --- .../{src => data}/cart-with-discount.ts | 2 +- .../data/get-redeemmables-for-validation.ts | 5 + .../data/has-at-least-one-redeemable.ts | 6 + packages/voucherify/data/persit.ts | 35 +++ packages/voucherify/package.json | 4 +- .../src/commerce-wrapper/add-cart-item.ts | 32 +++ .../src/commerce-wrapper/add-coupon.ts | 73 +++++ .../src/commerce-wrapper/create-cart.ts | 16 ++ .../src/commerce-wrapper/delete-cart-item.ts | 32 +++ .../src/commerce-wrapper/delete-coupon.ts | 37 +++ .../src/commerce-wrapper/get-cart.ts | 33 +++ .../voucherify/src/commerce-wrapper/index.ts | 44 +++ .../src/commerce-wrapper/update-cart-item.ts | 32 +++ packages/voucherify/src/index.ts | 260 +----------------- pnpm-lock.yaml | 6 + 15 files changed, 356 insertions(+), 261 deletions(-) rename packages/voucherify/{src => data}/cart-with-discount.ts (95%) create mode 100644 packages/voucherify/data/get-redeemmables-for-validation.ts create mode 100644 packages/voucherify/data/has-at-least-one-redeemable.ts create mode 100644 packages/voucherify/data/persit.ts create mode 100644 packages/voucherify/src/commerce-wrapper/add-cart-item.ts create mode 100644 packages/voucherify/src/commerce-wrapper/add-coupon.ts create mode 100644 packages/voucherify/src/commerce-wrapper/create-cart.ts create mode 100644 packages/voucherify/src/commerce-wrapper/delete-cart-item.ts create mode 100644 packages/voucherify/src/commerce-wrapper/delete-coupon.ts create mode 100644 packages/voucherify/src/commerce-wrapper/get-cart.ts create mode 100644 packages/voucherify/src/commerce-wrapper/index.ts create mode 100644 packages/voucherify/src/commerce-wrapper/update-cart-item.ts diff --git a/packages/voucherify/src/cart-with-discount.ts b/packages/voucherify/data/cart-with-discount.ts similarity index 95% rename from packages/voucherify/src/cart-with-discount.ts rename to packages/voucherify/data/cart-with-discount.ts index 291a07c..6ae00dc 100644 --- a/packages/voucherify/src/cart-with-discount.ts +++ b/packages/voucherify/data/cart-with-discount.ts @@ -5,7 +5,7 @@ import { Redeemable, } from '@composable/types' import { ValidationValidateStackableResponse } from '@voucherify/sdk' -import { centToString, toCent } from './to-cent' +import { centToString, toCent } from '../src/to-cent' export const cartWithDiscount = ( cart: Cart, diff --git a/packages/voucherify/data/get-redeemmables-for-validation.ts b/packages/voucherify/data/get-redeemmables-for-validation.ts new file mode 100644 index 0000000..df1b443 --- /dev/null +++ b/packages/voucherify/data/get-redeemmables-for-validation.ts @@ -0,0 +1,5 @@ +export const getRedeemmablesForValidation = (couponCodes: string[]) => + couponCodes.map((couponCode) => ({ + id: couponCode, + object: 'voucher' as const, + })) diff --git a/packages/voucherify/data/has-at-least-one-redeemable.ts b/packages/voucherify/data/has-at-least-one-redeemable.ts new file mode 100644 index 0000000..9e48603 --- /dev/null +++ b/packages/voucherify/data/has-at-least-one-redeemable.ts @@ -0,0 +1,6 @@ +import { getCartDiscounts } from './persit' + +export const hasAtLeastOneRedeemable = async (cartId: string) => { + const cartDiscountsStorage = await getCartDiscounts(cartId) + return cartDiscountsStorage && cartDiscountsStorage.length > 0 +} diff --git a/packages/voucherify/data/persit.ts b/packages/voucherify/data/persit.ts new file mode 100644 index 0000000..ba928fc --- /dev/null +++ b/packages/voucherify/data/persit.ts @@ -0,0 +1,35 @@ +import storage from 'node-persist' +import path from 'path' +import os from 'os' + +const storageFolderPath = path.join( + os.tmpdir(), + 'composable-ui-storage-voucherify' +) + +const localStarege = storage.create() + +localStarege.init({ + dir: storageFolderPath, +}) + +console.log( + `[voucherify][persist] Local storage in folder ${storageFolderPath}` +) + +export const getCartDiscounts = async (cartId: string): Promise => { + return (await localStarege.getItem(`cart-discounts-${cartId}`)) || [] +} + +export const saveCartDiscounts = async ( + cartId: string, + discounts: string[] +) => { + await localStarege.setItem(`cart-discounts-${cartId}`, discounts) + return discounts +} + +export const deleteCartDiscounts = async (cartId: string) => { + const result = await localStarege.del(`cart-discounts-${cartId}`) + return result.removed +} diff --git a/packages/voucherify/package.json b/packages/voucherify/package.json index c364b3d..8d16769 100644 --- a/packages/voucherify/package.json +++ b/packages/voucherify/package.json @@ -10,7 +10,9 @@ "ts": "tsc --noEmit --incremental" }, "dependencies": { - "@composable/types": "workspace:*" + "@composable/types": "workspace:*", + "@types/node-persist": "^3.1.5", + "node-persist": "^3.1.3" }, "devDependencies": { "eslint-config-custom": "workspace:*", diff --git a/packages/voucherify/src/commerce-wrapper/add-cart-item.ts b/packages/voucherify/src/commerce-wrapper/add-cart-item.ts new file mode 100644 index 0000000..ac9fff9 --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/add-cart-item.ts @@ -0,0 +1,32 @@ +import { CommerceService, CartWithDiscounts } from '@composable/types' +import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' +import { cartWithDiscount } from '../../data/cart-with-discount' +import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' +import { VoucherifyServerSide } from '@voucherify/sdk' +import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' +import { getCartDiscounts } from '../../data/persit' + +export const addCartItemFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.addCartItem(...props) + if (!cart) { + return cart + } + + const validationResponse = (await hasAtLeastOneRedeemable(props[0].cartId)) + ? await voucherify.validations.validateStackable({ + redeemables: getRedeemmablesForValidation( + await getCartDiscounts(props[0].cartId) + ), + order: cartToVoucherifyOrder(cart), + }) + : false + + return cartWithDiscount(cart, validationResponse) + } diff --git a/packages/voucherify/src/commerce-wrapper/add-coupon.ts b/packages/voucherify/src/commerce-wrapper/add-coupon.ts new file mode 100644 index 0000000..4577176 --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/add-coupon.ts @@ -0,0 +1,73 @@ +import { CommerceService, CartWithDiscounts } from '@composable/types' +import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' +import { cartWithDiscount } from '../../data/cart-with-discount' +import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' +import { + VoucherifyServerSide, + ValidationValidateStackableResponse, + StackableRedeemableResponse, +} from '@voucherify/sdk' +import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' +import { getCartDiscounts, saveCartDiscounts } from '../../data/persit' + +export const addCouponFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ({ cartId, coupon }: { cartId: string; coupon: string }) => { + let errorMsg: string | undefined + + const cartDiscounts = await getCartDiscounts(cartId) + + const cart = await commerceService.getCart({ cartId }) + + if (!cart) { + throw new Error('[voucherify][addCoupon ] cart not found') + } + + console.log( + `[voucherify][addCoupon] Add coupon ${coupon} to cart ${cartId}`, + cartDiscounts + ) + + console.log( + 'xxxxxx', + getRedeemmablesForValidation([...cartDiscounts, coupon]) + ) + + const validationResponse: // it's calculated incorrectly + | false + | (ValidationValidateStackableResponse & { + inapplicable_redeemables?: StackableRedeemableResponse[] + }) = await voucherify.validations.validateStackable({ + redeemables: getRedeemmablesForValidation([...cartDiscounts, coupon]), + order: cartToVoucherifyOrder(cart), + }) + + const addedRedeembale = + validationResponse && validationResponse.redeemables + ? [ + ...validationResponse.redeemables, + ...(validationResponse?.inapplicable_redeemables || []), + ]?.find((redeemable) => redeemable.id === coupon) + : false + + const result = addedRedeembale + ? addedRedeembale.status === 'APPLICABLE' + : false + + if (result) { + await saveCartDiscounts(cartId, [...cartDiscounts, coupon]) + } else { + errorMsg = addedRedeembale + ? addedRedeembale.result?.error?.message + : 'Redeemable not found in response from Voucherify' + } + + return { + cart: cartWithDiscount(cart, validationResponse), + result, + errorMsg, + } + } diff --git a/packages/voucherify/src/commerce-wrapper/create-cart.ts b/packages/voucherify/src/commerce-wrapper/create-cart.ts new file mode 100644 index 0000000..2b57da7 --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/create-cart.ts @@ -0,0 +1,16 @@ +import { CommerceService, CartWithDiscounts } from '@composable/types' +import { cartWithDiscount } from '../../data/cart-with-discount' +import { VoucherifyServerSide } from '@voucherify/sdk' + +export const createCartFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.createCart(...props) + + return cartWithDiscount(cart, false) + } diff --git a/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts b/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts new file mode 100644 index 0000000..6b8225f --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts @@ -0,0 +1,32 @@ +import { CommerceService, CartWithDiscounts } from '@composable/types' +import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' +import { cartWithDiscount } from '../../data/cart-with-discount' +import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' +import { VoucherifyServerSide } from '@voucherify/sdk' +import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' +import { getCartDiscounts } from '../../data/persit' + +export const deleteCartItemFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.deleteCartItem(...props) + if (!cart) { + return cart + } + + const validationResponse = (await hasAtLeastOneRedeemable(props[0].cartId)) + ? await voucherify.validations.validateStackable({ + redeemables: getRedeemmablesForValidation( + await getCartDiscounts(props[0].cartId) + ), + order: cartToVoucherifyOrder(cart), + }) + : false + + return cartWithDiscount(cart, validationResponse) + } diff --git a/packages/voucherify/src/commerce-wrapper/delete-coupon.ts b/packages/voucherify/src/commerce-wrapper/delete-coupon.ts new file mode 100644 index 0000000..11be9be --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/delete-coupon.ts @@ -0,0 +1,37 @@ +import { CommerceService, CartWithDiscounts } from '@composable/types' +import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' +import { cartWithDiscount } from '../../data/cart-with-discount' +import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' +import { VoucherifyServerSide } from '@voucherify/sdk' +import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' +import { getCartDiscounts } from '../../data/persit' + +export const deleteCouponFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ({ cartId, coupon }: { cartId: string; coupon: string }) => { + const cartDiscounts = (await getCartDiscounts(cartId)).filter( + (redeemable) => redeemable !== coupon + ) + + const cart = await commerceService.getCart({ cartId }) + + if (!cart) { + throw new Error('[voucherify][deleteCoupon] cart not found') + } + + console.log( + `[voucherify][deleteCoupon] Delete coupon ${coupon} from cart ${cartId}` + ) + + const validationResponse = (await hasAtLeastOneRedeemable(cartId)) + ? await voucherify.validations.validateStackable({ + redeemables: getRedeemmablesForValidation(cartDiscounts), + order: cartToVoucherifyOrder(cart), + }) + : false + + return cartWithDiscount(cart, validationResponse) + } diff --git a/packages/voucherify/src/commerce-wrapper/get-cart.ts b/packages/voucherify/src/commerce-wrapper/get-cart.ts new file mode 100644 index 0000000..488a967 --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/get-cart.ts @@ -0,0 +1,33 @@ +import { CommerceService, CartWithDiscounts } from '@composable/types' +import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' +import { cartWithDiscount } from '../../data/cart-with-discount' +import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' +import { VoucherifyServerSide } from '@voucherify/sdk' +import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' +import { getCartDiscounts } from '../../data/persit' + +export const getCartFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.getCart(...props) + + if (!cart) { + return cart + } + + const validationResponse = (await hasAtLeastOneRedeemable(props[0].cartId)) + ? await voucherify.validations.validateStackable({ + redeemables: getRedeemmablesForValidation( + await getCartDiscounts(props[0].cartId) + ), + order: cartToVoucherifyOrder(cart), + }) + : false + + return cartWithDiscount(cart, validationResponse) + } diff --git a/packages/voucherify/src/commerce-wrapper/index.ts b/packages/voucherify/src/commerce-wrapper/index.ts new file mode 100644 index 0000000..5c17a14 --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/index.ts @@ -0,0 +1,44 @@ +import { + CommerceService, + CommerceServiceWithDiscounts, +} from '@composable/types' +import { getCartFunction } from './get-cart' +import { VoucherifyServerSide } from '@voucherify/sdk' +import { addCartItemFunction } from './add-cart-item' +import { createCartFunction } from './create-cart' +import { deleteCartItemFunction } from './delete-cart-item' +import { updateCartItemFunction } from './update-cart-item' +import { addCouponFunction } from './add-coupon' +import { deleteCouponFunction } from './delete-coupon' + +if ( + !process.env.VOUCHERIFY_APPLICATION_ID || + !process.env.VOUCHERIFY_SECRET_KEY || + !process.env.VOUCHERIFY_API_URL +) { + throw new Error('[voucherify] Missing configuration') +} + +const voucherify = VoucherifyServerSide({ + applicationId: process.env.VOUCHERIFY_APPLICATION_ID, + secretKey: process.env.VOUCHERIFY_SECRET_KEY, + exposeErrorCause: true, + apiUrl: process.env.VOUCHERIFY_API_URL, + channel: 'ComposableUI', +}) + +export const commerceWithDiscount = ( + commerceService: CommerceService +): CommerceServiceWithDiscounts => { + console.log('[voucherify][commerceWithDiscount] wrapping commerce service') + return { + ...commerceService, + getCart: getCartFunction(commerceService, voucherify), + addCartItem: addCartItemFunction(commerceService, voucherify), + createCart: createCartFunction(commerceService, voucherify), + deleteCartItem: deleteCartItemFunction(commerceService, voucherify), + updateCartItem: updateCartItemFunction(commerceService, voucherify), + addCoupon: addCouponFunction(commerceService, voucherify), + deleteCoupon: deleteCouponFunction(commerceService, voucherify), + } +} diff --git a/packages/voucherify/src/commerce-wrapper/update-cart-item.ts b/packages/voucherify/src/commerce-wrapper/update-cart-item.ts new file mode 100644 index 0000000..9ec63ee --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/update-cart-item.ts @@ -0,0 +1,32 @@ +import { CommerceService, CartWithDiscounts } from '@composable/types' +import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' +import { cartWithDiscount } from '../../data/cart-with-discount' +import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' +import { VoucherifyServerSide } from '@voucherify/sdk' +import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' +import { getCartDiscounts } from '../../data/persit' + +export const updateCartItemFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ( + ...props: Parameters + ): Promise => { + const cart = await commerceService.updateCartItem(...props) + if (!cart) { + return cart + } + + const validationResponse = (await hasAtLeastOneRedeemable(props[0].cartId)) + ? await voucherify.validations.validateStackable({ + redeemables: getRedeemmablesForValidation( + await getCartDiscounts(props[0].cartId) + ), + order: cartToVoucherifyOrder(cart), + }) + : false + + return cartWithDiscount(cart, validationResponse) + } diff --git a/packages/voucherify/src/index.ts b/packages/voucherify/src/index.ts index 45c2c9d..0736442 100644 --- a/packages/voucherify/src/index.ts +++ b/packages/voucherify/src/index.ts @@ -1,259 +1 @@ -import { - Cart, - CartWithDiscounts, - CommerceService, - CommerceServiceWithDiscounts, -} from '@composable/types' -import { - ApplicableToResultList, - StackableRedeemableResponse, - ValidationValidateStackableResponse, - VoucherifyServerSide, -} from '@voucherify/sdk' -import { cartWithDiscount } from './cart-with-discount' -import { cartToVoucherifyOrder } from './cart-to-voucherify-order' - -if ( - !process.env.VOUCHERIFY_APPLICATION_ID || - !process.env.VOUCHERIFY_SECRET_KEY || - !process.env.VOUCHERIFY_API_URL -) { - throw new Error('[voucherify] Missing configuration') -} - -const voucherify = VoucherifyServerSide({ - applicationId: process.env.VOUCHERIFY_APPLICATION_ID, - secretKey: process.env.VOUCHERIFY_SECRET_KEY, - exposeErrorCause: true, - apiUrl: process.env.VOUCHERIFY_API_URL, - channel: 'ComposableUI', -}) - -type CartDiscountsStorage = { - [cartId: string]: string[] -} - -/** - * In memory storage that presist coupons for specific cart - */ -const cartDiscountsStorage: CartDiscountsStorage = { - '7a6dd462-24dc-11ed-861d-0242ac120002': ['10%OFF'], // example cart discount -} - -const hasAtLeastOneRedeemable = (cartId: string) => { - console.log({ cartId, cartDiscountsStorage }) - return cartDiscountsStorage[cartId] && cartDiscountsStorage[cartId].length -} - -const getRedeemmablesForValidation = (couponCodes: string[]) => - couponCodes.map((couponCode) => ({ - id: couponCode, - object: 'voucher' as const, - })) - -export const commerceWithDiscount = ( - commerceService: CommerceService -): CommerceServiceWithDiscounts => { - console.log('[voucherify] wrapping commerce service', cartDiscountsStorage) - - const getCart = async ( - ...props: Parameters - ): Promise => { - const cart = await commerceService.getCart(...props) - - if (!cart) { - return cart - } - - const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation( - cartDiscountsStorage[props[0].cartId] - ), - order: cartToVoucherifyOrder(cart), - }) - : false - - return cartWithDiscount(cart, validationResponse) - } - - const addCartItem = async ( - ...props: Parameters - ): Promise => { - const cart = await commerceService.addCartItem(...props) - if (!cart) { - return cart - } - - const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation( - cartDiscountsStorage[props[0].cartId] - ), - order: cartToVoucherifyOrder(cart), - }) - : false - - return cartWithDiscount(cart, validationResponse) - } - - const createCart = async ( - ...props: Parameters - ): Promise => { - const cart = await commerceService.createCart(...props) - - return cartWithDiscount(cart, false) - } - - const deleteCartItem = async ( - ...props: Parameters - ): Promise => { - const cart = await commerceService.deleteCartItem(...props) - if (!cart) { - return cart - } - - const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation( - cartDiscountsStorage[props[0].cartId] - ), - order: cartToVoucherifyOrder(cart), - }) - : false - - return cartWithDiscount(cart, validationResponse) - } - - const updateCartItem = async ( - ...props: Parameters - ): Promise => { - const cart = await commerceService.updateCartItem(...props) - if (!cart) { - return cart - } - - const validationResponse = hasAtLeastOneRedeemable(props[0].cartId) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation( - cartDiscountsStorage[props[0].cartId] - ), - order: cartToVoucherifyOrder(cart), - }) - : false - - return cartWithDiscount(cart, validationResponse) - } - - const addCoupon = async ({ - cartId, - coupon, - }: { - cartId: string - coupon: string - }) => { - let errorMsg: string | undefined - - if (!cartDiscountsStorage[cartId]) { - cartDiscountsStorage[cartId] = [] - } - - const cart = await commerceService.getCart({ cartId }) - - if (!cart) { - throw new Error('[voucherify][addCoupon ] cart not found') - } - - console.log( - `[voucherify][addCoupon] Add coupon ${coupon} to cart ${cartId}` - ) - - console.log( - 'xxxxxx', - getRedeemmablesForValidation([...cartDiscountsStorage[cartId], coupon]) - ) - - const validationResponse: // it's calculated incorrectly - | false - | (ValidationValidateStackableResponse & { - inapplicable_redeemables?: StackableRedeemableResponse[] - }) = await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation([ - ...cartDiscountsStorage[cartId], - coupon, - ]), - order: cartToVoucherifyOrder(cart), - }) - - const addedRedeembale = - validationResponse && validationResponse.redeemables - ? [ - ...validationResponse.redeemables, - ...(validationResponse?.inapplicable_redeemables || []), - ]?.find((redeemable) => redeemable.id === coupon) - : false - - const result = addedRedeembale - ? addedRedeembale.status === 'APPLICABLE' - : false - - if (result) { - cartDiscountsStorage[cartId].push(coupon) - } else { - errorMsg = addedRedeembale - ? addedRedeembale.result?.error?.message - : 'Redeemable not found in response from Voucherify' - } - - return { - cart: cartWithDiscount(cart, validationResponse), - result, - errorMsg, - } - } - - const deleteCoupon = async ({ - cartId, - coupon, - }: { - cartId: string - coupon: string - }) => { - if (cartDiscountsStorage[cartId]) { - cartDiscountsStorage[cartId] = cartDiscountsStorage[cartId].filter( - (redeemable) => redeemable !== coupon - ) - } - const cart = await commerceService.getCart({ cartId }) - - if (!cart) { - throw new Error('[voucherify][deleteCoupon] cart not found') - } - - console.log( - `[voucherify][deleteCoupon] Delete coupon ${coupon} from cart ${cartId}` - ) - - const validationResponse = hasAtLeastOneRedeemable(cartId) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation( - cartDiscountsStorage[cartId] - ), - order: cartToVoucherifyOrder(cart), - }) - : false - - return cartWithDiscount(cart, validationResponse) - } - - return { - ...commerceService, - getCart, - addCartItem, - createCart, - deleteCartItem, - updateCartItem, - addCoupon, - deleteCoupon, - } -} +export * from './commerce-wrapper' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f70d23..e8639f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,6 +355,12 @@ importers: '@composable/types': specifier: workspace:* version: link:../types + '@types/node-persist': + specifier: ^3.1.5 + version: 3.1.5 + node-persist: + specifier: ^3.1.3 + version: 3.1.3 devDependencies: eslint-config-custom: specifier: workspace:* From 1ac1568696be66acb007d5110a1df814ba4cb8f2 Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Tue, 31 Oct 2023 12:37:26 +0100 Subject: [PATCH 07/13] refactoring round two --- .../src/commerce-wrapper/add-cart-item.ts | 24 +++---- .../src/commerce-wrapper/add-coupon.ts | 65 +++++-------------- .../src/commerce-wrapper/delete-cart-item.ts | 24 +++---- .../src/commerce-wrapper/delete-coupon.ts | 31 ++++----- .../src/commerce-wrapper/get-cart.ts | 23 +++---- .../is-redeemable-applicable.ts | 27 ++++++++ .../src/commerce-wrapper/update-cart-item.ts | 24 +++---- packages/voucherify/src/validate-discounts.ts | 33 ++++++++++ 8 files changed, 127 insertions(+), 124 deletions(-) create mode 100644 packages/voucherify/src/commerce-wrapper/is-redeemable-applicable.ts create mode 100644 packages/voucherify/src/validate-discounts.ts diff --git a/packages/voucherify/src/commerce-wrapper/add-cart-item.ts b/packages/voucherify/src/commerce-wrapper/add-cart-item.ts index ac9fff9..6eaa518 100644 --- a/packages/voucherify/src/commerce-wrapper/add-cart-item.ts +++ b/packages/voucherify/src/commerce-wrapper/add-cart-item.ts @@ -1,10 +1,8 @@ import { CommerceService, CartWithDiscounts } from '@composable/types' -import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' import { cartWithDiscount } from '../../data/cart-with-discount' -import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' import { VoucherifyServerSide } from '@voucherify/sdk' -import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' import { getCartDiscounts } from '../../data/persit' +import { validateDiscounts } from '../validate-discounts' export const addCartItemFunction = ( @@ -15,18 +13,14 @@ export const addCartItemFunction = ...props: Parameters ): Promise => { const cart = await commerceService.addCartItem(...props) - if (!cart) { - return cart - } - const validationResponse = (await hasAtLeastOneRedeemable(props[0].cartId)) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation( - await getCartDiscounts(props[0].cartId) - ), - order: cartToVoucherifyOrder(cart), - }) - : false + const codes = await getCartDiscounts(props[0].cartId) - return cartWithDiscount(cart, validationResponse) + const validationResult = await validateDiscounts({ + voucherify, + cart, + codes, + }) + + return cartWithDiscount(cart, validationResult) } diff --git a/packages/voucherify/src/commerce-wrapper/add-coupon.ts b/packages/voucherify/src/commerce-wrapper/add-coupon.ts index 4577176..80550ac 100644 --- a/packages/voucherify/src/commerce-wrapper/add-coupon.ts +++ b/packages/voucherify/src/commerce-wrapper/add-coupon.ts @@ -1,14 +1,9 @@ -import { CommerceService, CartWithDiscounts } from '@composable/types' -import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' +import { CommerceService } from '@composable/types' import { cartWithDiscount } from '../../data/cart-with-discount' -import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' -import { - VoucherifyServerSide, - ValidationValidateStackableResponse, - StackableRedeemableResponse, -} from '@voucherify/sdk' -import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' +import { VoucherifyServerSide } from '@voucherify/sdk' import { getCartDiscounts, saveCartDiscounts } from '../../data/persit' +import { validateDiscounts } from '../validate-discounts' +import { isRedeemableApplicable } from './is-redeemable-applicable' export const addCouponFunction = ( @@ -16,58 +11,32 @@ export const addCouponFunction = voucherify: ReturnType ) => async ({ cartId, coupon }: { cartId: string; coupon: string }) => { - let errorMsg: string | undefined - - const cartDiscounts = await getCartDiscounts(cartId) - const cart = await commerceService.getCart({ cartId }) if (!cart) { - throw new Error('[voucherify][addCoupon ] cart not found') + throw new Error(`[voucherify][addCoupon] cart not found by id: ${cartId}`) } - console.log( - `[voucherify][addCoupon] Add coupon ${coupon} to cart ${cartId}`, - cartDiscounts - ) - - console.log( - 'xxxxxx', - getRedeemmablesForValidation([...cartDiscounts, coupon]) - ) + const cartDiscounts = await getCartDiscounts(cartId) - const validationResponse: // it's calculated incorrectly - | false - | (ValidationValidateStackableResponse & { - inapplicable_redeemables?: StackableRedeemableResponse[] - }) = await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation([...cartDiscounts, coupon]), - order: cartToVoucherifyOrder(cart), + const validationResponse = await validateDiscounts({ + cart, + voucherify, + codes: [...cartDiscounts, coupon], }) - const addedRedeembale = - validationResponse && validationResponse.redeemables - ? [ - ...validationResponse.redeemables, - ...(validationResponse?.inapplicable_redeemables || []), - ]?.find((redeemable) => redeemable.id === coupon) - : false - - const result = addedRedeembale - ? addedRedeembale.status === 'APPLICABLE' - : false + const { isApplicable, error } = isRedeemableApplicable( + coupon, + validationResponse + ) - if (result) { + if (isApplicable) { await saveCartDiscounts(cartId, [...cartDiscounts, coupon]) - } else { - errorMsg = addedRedeembale - ? addedRedeembale.result?.error?.message - : 'Redeemable not found in response from Voucherify' } return { cart: cartWithDiscount(cart, validationResponse), - result, - errorMsg, + result: isApplicable, + errorMsg: error, } } diff --git a/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts b/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts index 6b8225f..a07c65d 100644 --- a/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts +++ b/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts @@ -1,10 +1,8 @@ import { CommerceService, CartWithDiscounts } from '@composable/types' -import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' import { cartWithDiscount } from '../../data/cart-with-discount' -import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' import { VoucherifyServerSide } from '@voucherify/sdk' -import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' import { getCartDiscounts } from '../../data/persit' +import { validateDiscounts } from '../validate-discounts' export const deleteCartItemFunction = ( @@ -15,18 +13,14 @@ export const deleteCartItemFunction = ...props: Parameters ): Promise => { const cart = await commerceService.deleteCartItem(...props) - if (!cart) { - return cart - } - const validationResponse = (await hasAtLeastOneRedeemable(props[0].cartId)) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation( - await getCartDiscounts(props[0].cartId) - ), - order: cartToVoucherifyOrder(cart), - }) - : false + const codes = await getCartDiscounts(props[0].cartId) - return cartWithDiscount(cart, validationResponse) + const validationResult = await validateDiscounts({ + voucherify, + cart, + codes, + }) + + return cartWithDiscount(cart, validationResult) } diff --git a/packages/voucherify/src/commerce-wrapper/delete-coupon.ts b/packages/voucherify/src/commerce-wrapper/delete-coupon.ts index 11be9be..2372c60 100644 --- a/packages/voucherify/src/commerce-wrapper/delete-coupon.ts +++ b/packages/voucherify/src/commerce-wrapper/delete-coupon.ts @@ -1,10 +1,8 @@ -import { CommerceService, CartWithDiscounts } from '@composable/types' -import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' +import { CommerceService } from '@composable/types' import { cartWithDiscount } from '../../data/cart-with-discount' -import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' import { VoucherifyServerSide } from '@voucherify/sdk' -import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' -import { getCartDiscounts } from '../../data/persit' +import { getCartDiscounts, saveCartDiscounts } from '../../data/persit' +import { validateDiscounts } from '../validate-discounts' export const deleteCouponFunction = ( @@ -12,26 +10,23 @@ export const deleteCouponFunction = voucherify: ReturnType ) => async ({ cartId, coupon }: { cartId: string; coupon: string }) => { - const cartDiscounts = (await getCartDiscounts(cartId)).filter( - (redeemable) => redeemable !== coupon - ) - const cart = await commerceService.getCart({ cartId }) if (!cart) { throw new Error('[voucherify][deleteCoupon] cart not found') } - console.log( - `[voucherify][deleteCoupon] Delete coupon ${coupon} from cart ${cartId}` + const codes = (await getCartDiscounts(cartId)).filter( + (redeemable) => redeemable !== coupon ) - const validationResponse = (await hasAtLeastOneRedeemable(cartId)) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation(cartDiscounts), - order: cartToVoucherifyOrder(cart), - }) - : false + await saveCartDiscounts(cartId, codes) + + const validationResult = await validateDiscounts({ + voucherify, + cart, + codes, + }) - return cartWithDiscount(cart, validationResponse) + return cartWithDiscount(cart, validationResult) } diff --git a/packages/voucherify/src/commerce-wrapper/get-cart.ts b/packages/voucherify/src/commerce-wrapper/get-cart.ts index 488a967..458eefe 100644 --- a/packages/voucherify/src/commerce-wrapper/get-cart.ts +++ b/packages/voucherify/src/commerce-wrapper/get-cart.ts @@ -1,10 +1,8 @@ import { CommerceService, CartWithDiscounts } from '@composable/types' -import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' import { cartWithDiscount } from '../../data/cart-with-discount' -import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' import { VoucherifyServerSide } from '@voucherify/sdk' -import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' import { getCartDiscounts } from '../../data/persit' +import { validateDiscounts } from '../validate-discounts' export const getCartFunction = ( @@ -17,17 +15,16 @@ export const getCartFunction = const cart = await commerceService.getCart(...props) if (!cart) { - return cart + return null } - const validationResponse = (await hasAtLeastOneRedeemable(props[0].cartId)) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation( - await getCartDiscounts(props[0].cartId) - ), - order: cartToVoucherifyOrder(cart), - }) - : false + const codes = await getCartDiscounts(props[0].cartId) - return cartWithDiscount(cart, validationResponse) + const validationResult = await validateDiscounts({ + voucherify, + cart, + codes, + }) + + return cartWithDiscount(cart, validationResult) } diff --git a/packages/voucherify/src/commerce-wrapper/is-redeemable-applicable.ts b/packages/voucherify/src/commerce-wrapper/is-redeemable-applicable.ts new file mode 100644 index 0000000..24c2607 --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/is-redeemable-applicable.ts @@ -0,0 +1,27 @@ +import { ValidationResponse } from '../validate-discounts' + +export const isRedeemableApplicable = ( + coupon: string, + validationResult: ValidationResponse +): { isApplicable: boolean; error: undefined | string } => { + let error + const addedRedeembale = + validationResult && validationResult.redeemables + ? [ + ...validationResult.redeemables, + ...(validationResult?.inapplicable_redeemables || []), + ]?.find((redeemable) => redeemable.id === coupon) + : false + + const isApplicable = addedRedeembale + ? addedRedeembale.status === 'APPLICABLE' + : false + + if (!isApplicable) { + error = addedRedeembale + ? addedRedeembale.result?.error?.message + : 'Redeemable not found in response from Voucherify' + } + + return { isApplicable, error } +} diff --git a/packages/voucherify/src/commerce-wrapper/update-cart-item.ts b/packages/voucherify/src/commerce-wrapper/update-cart-item.ts index 9ec63ee..26da3c5 100644 --- a/packages/voucherify/src/commerce-wrapper/update-cart-item.ts +++ b/packages/voucherify/src/commerce-wrapper/update-cart-item.ts @@ -1,10 +1,8 @@ import { CommerceService, CartWithDiscounts } from '@composable/types' -import { cartToVoucherifyOrder } from '../cart-to-voucherify-order' import { cartWithDiscount } from '../../data/cart-with-discount' -import { hasAtLeastOneRedeemable } from '../../data/has-at-least-one-redeemable' import { VoucherifyServerSide } from '@voucherify/sdk' -import { getRedeemmablesForValidation } from '../../data/get-redeemmables-for-validation' import { getCartDiscounts } from '../../data/persit' +import { validateDiscounts } from '../validate-discounts' export const updateCartItemFunction = ( @@ -15,18 +13,14 @@ export const updateCartItemFunction = ...props: Parameters ): Promise => { const cart = await commerceService.updateCartItem(...props) - if (!cart) { - return cart - } - const validationResponse = (await hasAtLeastOneRedeemable(props[0].cartId)) - ? await voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation( - await getCartDiscounts(props[0].cartId) - ), - order: cartToVoucherifyOrder(cart), - }) - : false + const codes = await getCartDiscounts(props[0].cartId) - return cartWithDiscount(cart, validationResponse) + const validationResult = await validateDiscounts({ + voucherify, + cart, + codes, + }) + + return cartWithDiscount(cart, validationResult) } diff --git a/packages/voucherify/src/validate-discounts.ts b/packages/voucherify/src/validate-discounts.ts new file mode 100644 index 0000000..c754afe --- /dev/null +++ b/packages/voucherify/src/validate-discounts.ts @@ -0,0 +1,33 @@ +import { Cart } from '@composable/types' +import { + StackableRedeemableResponse, + ValidationValidateStackableResponse, + VoucherifyServerSide, +} from '@voucherify/sdk' +import { getRedeemmablesForValidation } from '../data/get-redeemmables-for-validation' +import { cartToVoucherifyOrder } from './cart-to-voucherify-order' + +type ValidateDiscountsParam = { + cart: Cart + codes: string[] + voucherify: ReturnType +} + +export type ValidationResponse = + | false + | (ValidationValidateStackableResponse & { + inapplicable_redeemables?: StackableRedeemableResponse[] + }) + +export const validateDiscounts = async ( + params: ValidateDiscountsParam +): Promise => { + const { cart, codes, voucherify } = params + if (!codes.length) { + return false + } + return voucherify.validations.validateStackable({ + redeemables: getRedeemmablesForValidation(codes), + order: cartToVoucherifyOrder(cart), + }) +} From 0b079e6c9f9e342e78e7b7915c41a4bc8b194f72 Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Tue, 31 Oct 2023 16:53:31 +0100 Subject: [PATCH 08/13] upload products to Voucherify --- scripts/.env.example | 6 ++- scripts/package.json | 3 +- scripts/src/voucherify-setup/config.ts | 6 +++ scripts/src/voucherify-setup/index.ts | 62 ++++++++++++++++++++++ scripts/src/voucherify-setup/voucherify.ts | 14 +++++ 5 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 scripts/src/voucherify-setup/config.ts create mode 100644 scripts/src/voucherify-setup/index.ts create mode 100644 scripts/src/voucherify-setup/voucherify.ts diff --git a/scripts/.env.example b/scripts/.env.example index 2e67426..8fea423 100644 --- a/scripts/.env.example +++ b/scripts/.env.example @@ -1,3 +1,7 @@ ALGOLIA_APP_ID= ALGOLIA_API_ADMIN_KEY= -ALGOLIA_INDEX_NAME=products \ No newline at end of file +ALGOLIA_INDEX_NAME=products + +VOUCHERIFY_API_URL= +VOUCHERIFY_APPLICATION_ID= +VOUCHERIFY_SECRET_KEY= \ No newline at end of file diff --git a/scripts/package.json b/scripts/package.json index 3c23f51..777ad23 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -3,7 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { - "algolia-setup": "ts-node src/algolia-setup/index.ts" + "algolia-setup": "ts-node src/algolia-setup/index.ts", + "voucherify-setup": "ts-node src/voucherify-setup/index.ts" }, "devDependencies": { "@types/node": "^18.6.3", diff --git a/scripts/src/voucherify-setup/config.ts b/scripts/src/voucherify-setup/config.ts new file mode 100644 index 0000000..592febd --- /dev/null +++ b/scripts/src/voucherify-setup/config.ts @@ -0,0 +1,6 @@ +import { config } from 'dotenv' +config() + +export const VOUCHERIFY_API_URL = process.env.VOUCHERIFY_API_URL +export const VOUCHERIFY_APPLICATION_ID = process.env.VOUCHERIFY_APPLICATION_ID +export const VOUCHERIFY_SECRET_KEY = process.env.VOUCHERIFY_SECRET_KEY diff --git a/scripts/src/voucherify-setup/index.ts b/scripts/src/voucherify-setup/index.ts new file mode 100644 index 0000000..a48dcbb --- /dev/null +++ b/scripts/src/voucherify-setup/index.ts @@ -0,0 +1,62 @@ +import { voucherifyClient } from './voucherify' +import { + VOUCHERIFY_API_URL, + VOUCHERIFY_APPLICATION_ID, + VOUCHERIFY_SECRET_KEY, +} from './config' + +import products from '../../../packages/commerce-generic/src/data/products.json' + +const VOUCHERIFY_KEYS = [ + VOUCHERIFY_API_URL, + VOUCHERIFY_APPLICATION_ID, + VOUCHERIFY_SECRET_KEY, +] +const voucherifyKeysMissing = VOUCHERIFY_KEYS.some((key) => !key) + +const voucherifySetup = async () => { + console.log('Starting setting up Voucherify!') + try { + if (voucherifyKeysMissing) { + console.error( + 'You are missing some Voucherify keys in your .env file.', + `You must set the following:VOUCHERIFY_API_URL, VOUCHERIFY_APPLICATION_ID, VOUCHERIFY_SECRET_KEY.` + ) + throw new Error('VOUCHERIFY_MISSING_KEYS') + } + + for (const product of products) { + const createdProduct = await voucherifyClient.products.create({ + name: product.name, + source_id: product.id, + price: product.price, + image_url: product.images[0].url, + metadata: { + brand: product.brand, + category: product.category, + description: product.description, + materialAndCare: product.materialAndCare, + slug: product.slug, + type: product.type, + }, + }) + const createdSKU = await voucherifyClient.products.createSku( + createdProduct.id, + { + sku: product.sku, + } + ) + + console.log(`Created product ${product.id} and sku ${createdSKU.id}`) + } + + console.log('Finished setting up Voucherify!') + } catch (err) { + console.error(err.message) + throw err + } +} + +;(async () => { + await voucherifySetup() +})() diff --git a/scripts/src/voucherify-setup/voucherify.ts b/scripts/src/voucherify-setup/voucherify.ts new file mode 100644 index 0000000..c0bb3aa --- /dev/null +++ b/scripts/src/voucherify-setup/voucherify.ts @@ -0,0 +1,14 @@ +import { VoucherifyServerSide } from '@voucherify/sdk' +import { + VOUCHERIFY_API_URL, + VOUCHERIFY_APPLICATION_ID, + VOUCHERIFY_SECRET_KEY, +} from './config' + +export const voucherifyClient = VoucherifyServerSide({ + applicationId: VOUCHERIFY_APPLICATION_ID, + secretKey: VOUCHERIFY_SECRET_KEY, + exposeErrorCause: true, + apiUrl: VOUCHERIFY_API_URL, + channel: 'ComposableUI', +}) From 0726e9c4181b9b51faf270784e2d3d88bb5ca18a Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Thu, 2 Nov 2023 16:27:43 +0100 Subject: [PATCH 09/13] PoC Promotions --- .../src/components/forms/coupon-form.tsx | 20 +++++----- .../src/voucherify/cart-with-discounts.ts | 1 + .../voucherify/data/cart-with-discount.ts | 24 ++++++++++-- .../data/get-redeemmables-for-validation.ts | 10 +++++ .../src/cart-to-voucherify-order.ts | 2 +- .../src/commerce-wrapper/add-cart-item.ts | 15 ++++---- .../src/commerce-wrapper/add-coupon.ts | 17 +++++---- .../src/commerce-wrapper/create-cart.ts | 2 +- .../src/commerce-wrapper/delete-cart-item.ts | 15 ++++---- .../src/commerce-wrapper/delete-coupon.ts | 15 ++++---- .../src/commerce-wrapper/get-cart.ts | 15 ++++---- .../is-redeemable-applicable.ts | 4 +- .../src/commerce-wrapper/update-cart-item.ts | 15 ++++---- packages/voucherify/src/validate-discounts.ts | 38 ++++++++++++++----- 14 files changed, 124 insertions(+), 69 deletions(-) diff --git a/composable-ui/src/components/forms/coupon-form.tsx b/composable-ui/src/components/forms/coupon-form.tsx index 5747768..004c57e 100644 --- a/composable-ui/src/components/forms/coupon-form.tsx +++ b/composable-ui/src/components/forms/coupon-form.tsx @@ -108,15 +108,17 @@ export const CouponForm = ({ borderRadius="md" variant="outline" > - {redeemable.id} - - deleteCartCoupon.mutate({ - cartId: cart.id || '', - coupon: redeemable.id, - }) - } - /> + {redeemable.label} + {redeemable.object === 'voucher' && ( + + deleteCartCoupon.mutate({ + cartId: cart.id || '', + coupon: redeemable.id, + }) + } + /> + )} ))} diff --git a/packages/types/src/voucherify/cart-with-discounts.ts b/packages/types/src/voucherify/cart-with-discounts.ts index f67c43d..1771c8b 100644 --- a/packages/types/src/voucherify/cart-with-discounts.ts +++ b/packages/types/src/voucherify/cart-with-discounts.ts @@ -34,4 +34,5 @@ export type Redeemable = { id: string status: string object: string + label?: string } diff --git a/packages/voucherify/data/cart-with-discount.ts b/packages/voucherify/data/cart-with-discount.ts index 6ae00dc..ed1e48d 100644 --- a/packages/voucherify/data/cart-with-discount.ts +++ b/packages/voucherify/data/cart-with-discount.ts @@ -4,16 +4,32 @@ import { CartWithDiscounts, Redeemable, } from '@composable/types' -import { ValidationValidateStackableResponse } from '@voucherify/sdk' +import { + PromotionsValidateResponse, + ValidationValidateStackableResponse, +} from '@voucherify/sdk' import { centToString, toCent } from '../src/to-cent' export const cartWithDiscount = ( cart: Cart, - validationResponse: ValidationValidateStackableResponse | false + validationResponse: ValidationValidateStackableResponse | false, + promotionsResult: PromotionsValidateResponse | false ): CartWithDiscounts => { const redeemables: Redeemable[] = validationResponse - ? validationResponse.redeemables || [] - : [] // todo filter onlyr equired attributes + ? validationResponse.redeemables?.map((redeemable) => ({ + id: redeemable.id, + status: redeemable.status, + object: redeemable.object, + label: + redeemable.object === 'promotion_tier' + ? promotionsResult + ? promotionsResult.promotions?.find( + (promotion) => promotion.id === redeemable.id + )?.banner + : redeemable.id + : redeemable.id, + })) || [] + : [] const items: CartItemWithDiscounts[] = cart.items.map((item) => ({ ...item, cartItemType: 'CartItemWithDiscounts', diff --git a/packages/voucherify/data/get-redeemmables-for-validation.ts b/packages/voucherify/data/get-redeemmables-for-validation.ts index df1b443..2b845a6 100644 --- a/packages/voucherify/data/get-redeemmables-for-validation.ts +++ b/packages/voucherify/data/get-redeemmables-for-validation.ts @@ -1,5 +1,15 @@ +import { PromotionsValidateResponse } from '@voucherify/sdk' + export const getRedeemmablesForValidation = (couponCodes: string[]) => couponCodes.map((couponCode) => ({ id: couponCode, object: 'voucher' as const, })) + +export const getRedeemmablesForValidationFromPromotions = ( + promotionResult: PromotionsValidateResponse +) => + promotionResult.promotions?.map((promotion) => ({ + id: promotion.id, + object: 'promotion_tier' as const, + })) || [] diff --git a/packages/voucherify/src/cart-to-voucherify-order.ts b/packages/voucherify/src/cart-to-voucherify-order.ts index 7715635..1fc3111 100644 --- a/packages/voucherify/src/cart-to-voucherify-order.ts +++ b/packages/voucherify/src/cart-to-voucherify-order.ts @@ -9,7 +9,7 @@ export const cartToVoucherifyOrder = (cart: Cart): OrdersCreate => { quantity: item.quantity, product_id: item.id, sku_id: item.sku, - price: item.price, + price: item.price * 100, })), } } diff --git a/packages/voucherify/src/commerce-wrapper/add-cart-item.ts b/packages/voucherify/src/commerce-wrapper/add-cart-item.ts index 6eaa518..48cdc0c 100644 --- a/packages/voucherify/src/commerce-wrapper/add-cart-item.ts +++ b/packages/voucherify/src/commerce-wrapper/add-cart-item.ts @@ -2,7 +2,7 @@ import { CommerceService, CartWithDiscounts } from '@composable/types' import { cartWithDiscount } from '../../data/cart-with-discount' import { VoucherifyServerSide } from '@voucherify/sdk' import { getCartDiscounts } from '../../data/persit' -import { validateDiscounts } from '../validate-discounts' +import { validateCouponsAndPromotions } from '../validate-discounts' export const addCartItemFunction = ( @@ -16,11 +16,12 @@ export const addCartItemFunction = const codes = await getCartDiscounts(props[0].cartId) - const validationResult = await validateDiscounts({ - voucherify, - cart, - codes, - }) + const { validationResult, promotionsResult } = + await validateCouponsAndPromotions({ + voucherify, + cart, + codes, + }) - return cartWithDiscount(cart, validationResult) + return cartWithDiscount(cart, validationResult, promotionsResult) } diff --git a/packages/voucherify/src/commerce-wrapper/add-coupon.ts b/packages/voucherify/src/commerce-wrapper/add-coupon.ts index 80550ac..7472937 100644 --- a/packages/voucherify/src/commerce-wrapper/add-coupon.ts +++ b/packages/voucherify/src/commerce-wrapper/add-coupon.ts @@ -2,7 +2,7 @@ import { CommerceService } from '@composable/types' import { cartWithDiscount } from '../../data/cart-with-discount' import { VoucherifyServerSide } from '@voucherify/sdk' import { getCartDiscounts, saveCartDiscounts } from '../../data/persit' -import { validateDiscounts } from '../validate-discounts' +import { validateCouponsAndPromotions } from '../validate-discounts' import { isRedeemableApplicable } from './is-redeemable-applicable' export const addCouponFunction = @@ -19,15 +19,16 @@ export const addCouponFunction = const cartDiscounts = await getCartDiscounts(cartId) - const validationResponse = await validateDiscounts({ - cart, - voucherify, - codes: [...cartDiscounts, coupon], - }) + const { validationResult, promotionsResult } = + await validateCouponsAndPromotions({ + cart, + voucherify, + codes: [...cartDiscounts, coupon], + }) const { isApplicable, error } = isRedeemableApplicable( coupon, - validationResponse + validationResult ) if (isApplicable) { @@ -35,7 +36,7 @@ export const addCouponFunction = } return { - cart: cartWithDiscount(cart, validationResponse), + cart: cartWithDiscount(cart, validationResult, promotionsResult), result: isApplicable, errorMsg: error, } diff --git a/packages/voucherify/src/commerce-wrapper/create-cart.ts b/packages/voucherify/src/commerce-wrapper/create-cart.ts index 2b57da7..264f60c 100644 --- a/packages/voucherify/src/commerce-wrapper/create-cart.ts +++ b/packages/voucherify/src/commerce-wrapper/create-cart.ts @@ -12,5 +12,5 @@ export const createCartFunction = ): Promise => { const cart = await commerceService.createCart(...props) - return cartWithDiscount(cart, false) + return cartWithDiscount(cart, false, false) } diff --git a/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts b/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts index a07c65d..52d30e6 100644 --- a/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts +++ b/packages/voucherify/src/commerce-wrapper/delete-cart-item.ts @@ -2,7 +2,7 @@ import { CommerceService, CartWithDiscounts } from '@composable/types' import { cartWithDiscount } from '../../data/cart-with-discount' import { VoucherifyServerSide } from '@voucherify/sdk' import { getCartDiscounts } from '../../data/persit' -import { validateDiscounts } from '../validate-discounts' +import { validateCouponsAndPromotions } from '../validate-discounts' export const deleteCartItemFunction = ( @@ -16,11 +16,12 @@ export const deleteCartItemFunction = const codes = await getCartDiscounts(props[0].cartId) - const validationResult = await validateDiscounts({ - voucherify, - cart, - codes, - }) + const { validationResult, promotionsResult } = + await validateCouponsAndPromotions({ + voucherify, + cart, + codes, + }) - return cartWithDiscount(cart, validationResult) + return cartWithDiscount(cart, validationResult, promotionsResult) } diff --git a/packages/voucherify/src/commerce-wrapper/delete-coupon.ts b/packages/voucherify/src/commerce-wrapper/delete-coupon.ts index 2372c60..678529b 100644 --- a/packages/voucherify/src/commerce-wrapper/delete-coupon.ts +++ b/packages/voucherify/src/commerce-wrapper/delete-coupon.ts @@ -2,7 +2,7 @@ import { CommerceService } from '@composable/types' import { cartWithDiscount } from '../../data/cart-with-discount' import { VoucherifyServerSide } from '@voucherify/sdk' import { getCartDiscounts, saveCartDiscounts } from '../../data/persit' -import { validateDiscounts } from '../validate-discounts' +import { validateCouponsAndPromotions } from '../validate-discounts' export const deleteCouponFunction = ( @@ -22,11 +22,12 @@ export const deleteCouponFunction = await saveCartDiscounts(cartId, codes) - const validationResult = await validateDiscounts({ - voucherify, - cart, - codes, - }) + const { validationResult, promotionsResult } = + await validateCouponsAndPromotions({ + voucherify, + cart, + codes, + }) - return cartWithDiscount(cart, validationResult) + return cartWithDiscount(cart, validationResult, promotionsResult) } diff --git a/packages/voucherify/src/commerce-wrapper/get-cart.ts b/packages/voucherify/src/commerce-wrapper/get-cart.ts index 458eefe..9932b97 100644 --- a/packages/voucherify/src/commerce-wrapper/get-cart.ts +++ b/packages/voucherify/src/commerce-wrapper/get-cart.ts @@ -2,7 +2,7 @@ import { CommerceService, CartWithDiscounts } from '@composable/types' import { cartWithDiscount } from '../../data/cart-with-discount' import { VoucherifyServerSide } from '@voucherify/sdk' import { getCartDiscounts } from '../../data/persit' -import { validateDiscounts } from '../validate-discounts' +import { validateCouponsAndPromotions } from '../validate-discounts' export const getCartFunction = ( @@ -20,11 +20,12 @@ export const getCartFunction = const codes = await getCartDiscounts(props[0].cartId) - const validationResult = await validateDiscounts({ - voucherify, - cart, - codes, - }) + const { validationResult, promotionsResult } = + await validateCouponsAndPromotions({ + voucherify, + cart, + codes, + }) - return cartWithDiscount(cart, validationResult) + return cartWithDiscount(cart, validationResult, promotionsResult) } diff --git a/packages/voucherify/src/commerce-wrapper/is-redeemable-applicable.ts b/packages/voucherify/src/commerce-wrapper/is-redeemable-applicable.ts index 24c2607..6e25451 100644 --- a/packages/voucherify/src/commerce-wrapper/is-redeemable-applicable.ts +++ b/packages/voucherify/src/commerce-wrapper/is-redeemable-applicable.ts @@ -1,8 +1,8 @@ -import { ValidationResponse } from '../validate-discounts' +import { ValidateStackableResult } from '../validate-discounts' export const isRedeemableApplicable = ( coupon: string, - validationResult: ValidationResponse + validationResult: ValidateStackableResult ): { isApplicable: boolean; error: undefined | string } => { let error const addedRedeembale = diff --git a/packages/voucherify/src/commerce-wrapper/update-cart-item.ts b/packages/voucherify/src/commerce-wrapper/update-cart-item.ts index 26da3c5..20406c0 100644 --- a/packages/voucherify/src/commerce-wrapper/update-cart-item.ts +++ b/packages/voucherify/src/commerce-wrapper/update-cart-item.ts @@ -2,7 +2,7 @@ import { CommerceService, CartWithDiscounts } from '@composable/types' import { cartWithDiscount } from '../../data/cart-with-discount' import { VoucherifyServerSide } from '@voucherify/sdk' import { getCartDiscounts } from '../../data/persit' -import { validateDiscounts } from '../validate-discounts' +import { validateCouponsAndPromotions } from '../validate-discounts' export const updateCartItemFunction = ( @@ -16,11 +16,12 @@ export const updateCartItemFunction = const codes = await getCartDiscounts(props[0].cartId) - const validationResult = await validateDiscounts({ - voucherify, - cart, - codes, - }) + const { validationResult, promotionsResult } = + await validateCouponsAndPromotions({ + voucherify, + cart, + codes, + }) - return cartWithDiscount(cart, validationResult) + return cartWithDiscount(cart, validationResult, promotionsResult) } diff --git a/packages/voucherify/src/validate-discounts.ts b/packages/voucherify/src/validate-discounts.ts index c754afe..1df20cf 100644 --- a/packages/voucherify/src/validate-discounts.ts +++ b/packages/voucherify/src/validate-discounts.ts @@ -1,10 +1,14 @@ import { Cart } from '@composable/types' import { + PromotionsValidateResponse, StackableRedeemableResponse, ValidationValidateStackableResponse, VoucherifyServerSide, } from '@voucherify/sdk' -import { getRedeemmablesForValidation } from '../data/get-redeemmables-for-validation' +import { + getRedeemmablesForValidation, + getRedeemmablesForValidationFromPromotions, +} from '../data/get-redeemmables-for-validation' import { cartToVoucherifyOrder } from './cart-to-voucherify-order' type ValidateDiscountsParam = { @@ -13,21 +17,37 @@ type ValidateDiscountsParam = { voucherify: ReturnType } -export type ValidationResponse = +export type validateCouponsAndPromotionsResponse = { + promotionsResult: PromotionsValidateResponse + validationResult: ValidateStackableResult +} + +export type ValidateStackableResult = | false | (ValidationValidateStackableResponse & { inapplicable_redeemables?: StackableRedeemableResponse[] }) -export const validateDiscounts = async ( +export const validateCouponsAndPromotions = async ( params: ValidateDiscountsParam -): Promise => { +): Promise => { const { cart, codes, voucherify } = params - if (!codes.length) { - return false + + const order = cartToVoucherifyOrder(cart) + + const promotionsResult = await voucherify.promotions.validate({ order }) + + if (!codes.length && !promotionsResult.promotions?.length) { + return { promotionsResult, validationResult: false } } - return voucherify.validations.validateStackable({ - redeemables: getRedeemmablesForValidation(codes), - order: cartToVoucherifyOrder(cart), + + const validationResult = await voucherify.validations.validateStackable({ + redeemables: [ + ...getRedeemmablesForValidation(codes), + ...getRedeemmablesForValidationFromPromotions(promotionsResult), + ], + order, }) + + return { promotionsResult, validationResult } } From 7fc7458a3c1dea95f94e481c5994ad13be94c2eb Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Thu, 2 Nov 2023 16:58:58 +0100 Subject: [PATCH 10/13] fix build error --- .../src/components/cart/__data__/cart-data.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/composable-ui/src/components/cart/__data__/cart-data.ts b/composable-ui/src/components/cart/__data__/cart-data.ts index a8e7a36..4acd763 100644 --- a/composable-ui/src/components/cart/__data__/cart-data.ts +++ b/composable-ui/src/components/cart/__data__/cart-data.ts @@ -1,10 +1,14 @@ +import { CartItemWithDiscounts } from '@composable/types' import { CartData } from '../../../hooks' export const cartData: CartData = { id: '7a6dd462-24dc-11ed-861d-0242ac120002', + cartType: 'CartWithDiscounts', + redeemables: [], items: [ { id: '1', + cartItemType: 'CartItemWithDiscounts', category: 'Accessories', type: 'Bag', name: 'Venture Daypack', @@ -17,12 +21,18 @@ export const cartData: CartData = { sku: 'SKU-A1-2345', slug: 'venture-daypack', quantity: 1, + discounts: { + subtotalAmount: '', + }, }, - ], + ] as CartItemWithDiscounts[], summary: { taxes: '2.45', totalPrice: '35.00', shipping: 'Free', + discountAmount: '0', + totalDiscountAmount: '0', + grandPrice: '0', }, isLoading: false, isEmpty: false, From d35191c9b65c545b0798dd3c7f854c54dde81fd9 Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Fri, 3 Nov 2023 10:09:50 +0100 Subject: [PATCH 11/13] - display vouchers and promotions seperatelly - show discount per voucher/promotions - some styling --- .../src/components/cart/cart-promotions.tsx | 66 ++++++++++ .../src/components/cart/cart-summary.tsx | 11 ++ .../src/components/forms/coupon-form.tsx | 117 +++++++++++------- composable-ui/src/server/intl/en-US.json | 2 + .../src/voucherify/cart-with-discounts.ts | 3 +- .../voucherify/data/cart-with-discount.ts | 6 + .../src/commerce-wrapper/get-cart.ts | 4 +- packages/voucherify/src/validate-discounts.ts | 1 + 8 files changed, 161 insertions(+), 49 deletions(-) create mode 100644 composable-ui/src/components/cart/cart-promotions.tsx diff --git a/composable-ui/src/components/cart/cart-promotions.tsx b/composable-ui/src/components/cart/cart-promotions.tsx new file mode 100644 index 0000000..c559c1a --- /dev/null +++ b/composable-ui/src/components/cart/cart-promotions.tsx @@ -0,0 +1,66 @@ +import { useIntl } from 'react-intl' +import { CartItem, Redeemable } from '@composable/types' +import { + Box, + Divider, + Flex, + HStack, + Link, + StackDivider, + Tag, + TagCloseButton, + TagLabel, + Wrap, + WrapItem, + Text, + useBreakpointValue, + TagLeftIcon, +} from '@chakra-ui/react' +import { Icon } from '@chakra-ui/icons' +import { MdShoppingCart } from 'react-icons/md' +import { Price } from 'components/price' +import { QuantityPicker } from 'components/quantity-picker' +import { CartItemData, CartSummaryItem } from '.' + +interface CartPromotionsProps { + promotions: Redeemable[] +} + +export const CartPromotions = ({ promotions }: CartPromotionsProps) => { + const intl = useIntl() + // const isMobile = useBreakpointValue({ base: true, md: false }) + if (!promotions.length) { + return null + } + + return ( + <> + + {promotions.map((redeemable) => ( + + + + {redeemable.label} + + + + + + ))} + + ) +} diff --git a/composable-ui/src/components/cart/cart-summary.tsx b/composable-ui/src/components/cart/cart-summary.tsx index 8a243f7..be38b4b 100644 --- a/composable-ui/src/components/cart/cart-summary.tsx +++ b/composable-ui/src/components/cart/cart-summary.tsx @@ -13,6 +13,7 @@ import { Text, } from '@chakra-ui/react' import { CartSummaryItem } from '.' +import { CartPromotions } from './cart-promotions' interface CartSummaryProps { rootProps?: StackProps @@ -30,6 +31,15 @@ export const CartSummary = ({ const intl = useIntl() const _cartData = cartData ?? cart + const vouchers = + _cartData.redeemables?.filter( + (redeemable) => redeemable.object === 'voucher' + ) || [] + const promotions = + _cartData.redeemables?.filter( + (redeemable) => redeemable.object === 'promotion_tier' + ) || [] + return ( @@ -91,6 +101,7 @@ export const CartSummary = ({ )} + {_cartData.summary?.totalDiscountAmount && ( > - signIn?: typeof signIn - type?: AccountPage -} +import { Price } from 'components/price' +import { CartSummaryItem } from 'components/cart' +import { Icon } from '@chakra-ui/react' +import { MdDiscount } from 'react-icons/md' +import { displayValue } from '@tanstack/react-query-devtools/build/lib/utils' -export const CouponForm = ({ - signIn, - type = AccountPage.PAGE, - setAccountFormToShow, -}: LoginFormProps) => { +export const CouponForm = () => { const intl = useIntl() - const [isError, setIsError] = useState(false) + const [errorMessage, setErrorMessage] = useState(false) const { register, handleSubmit, @@ -37,7 +38,7 @@ export const CouponForm = ({ }) const { cart, addCartCoupon, deleteCartCoupon } = useCart({ onCartCouponAddError: (msg) => { - setError('coupon', { message: msg || 'Could not add coupon' }) + setErrorMessage(msg || 'Could not add coupon') }, }) @@ -53,18 +54,21 @@ export const CouponForm = ({ }, } + const vouchers = + cart.redeemables?.filter((redeemable) => redeemable.object === 'voucher') || + [] + return ( - - {isError && ( - - - asdasdasd - - )} + <> + { - setIsError(false) + setErrorMessage(false) // setError('coupon', {message: 'Could not add coupon' }) await addCartCoupon.mutate({ @@ -77,19 +81,23 @@ export const CouponForm = ({ - } type="submit" @@ -97,32 +105,47 @@ export const CouponForm = ({ variant={'outline'} /> + {errorMessage && ( + + + {errorMessage} + + )} - - {cart.redeemables?.map((redeemable) => ( + {vouchers.map((redeemable) => ( + + {redeemable.label} - {redeemable.object === 'voucher' && ( - - deleteCartCoupon.mutate({ - cartId: cart.id || '', - coupon: redeemable.id, - }) - } - /> - )} + + deleteCartCoupon.mutate({ + cartId: cart.id || '', + coupon: redeemable.id, + }) + } + /> - ))} - - + + + + + ))} + ) } diff --git a/composable-ui/src/server/intl/en-US.json b/composable-ui/src/server/intl/en-US.json index 645da70..21e6c5e 100644 --- a/composable-ui/src/server/intl/en-US.json +++ b/composable-ui/src/server/intl/en-US.json @@ -119,6 +119,7 @@ "cart.summary.estimatedTotal": "Estimated Total", "cart.summary.orderTotal": "Order Total", "cart.summary.totalDiscountAmount": "All discounts", + "cart.summary.promotions": "Promotions", "cart.summary.grandPrice": "Grand Total", "cart.summary.shipping.complimentaryDelivery": "Complimentary Delivery", "cart.summary.shipping.free": "Free", @@ -126,6 +127,7 @@ "cart.summary.subtotal": "Subtotal", "cart.summary.tax": "Tax", "cart.summary.taxes": "Taxes", + "cart.summary.couponCodes": "Coupon Codes", "cart.summary.title": "Order Summary", "cart.summary.total": "Total", "cart.summary.label.coupon": "Coupon code", diff --git a/packages/types/src/voucherify/cart-with-discounts.ts b/packages/types/src/voucherify/cart-with-discounts.ts index 1771c8b..20fd9fa 100644 --- a/packages/types/src/voucherify/cart-with-discounts.ts +++ b/packages/types/src/voucherify/cart-with-discounts.ts @@ -33,6 +33,7 @@ export type CartWithDiscounts = Cart & { export type Redeemable = { id: string status: string - object: string + object: 'voucher' | 'promotion_tier' | 'promotion_stack' label?: string + discount: string } diff --git a/packages/voucherify/data/cart-with-discount.ts b/packages/voucherify/data/cart-with-discount.ts index ed1e48d..02a9df5 100644 --- a/packages/voucherify/data/cart-with-discount.ts +++ b/packages/voucherify/data/cart-with-discount.ts @@ -20,6 +20,12 @@ export const cartWithDiscount = ( id: redeemable.id, status: redeemable.status, object: redeemable.object, + discount: centToString( + redeemable.order?.total_applied_discount_amount || + redeemable.result?.discount?.amount_off || + redeemable.result?.discount?.percent_off || + 0 + ), label: redeemable.object === 'promotion_tier' ? promotionsResult diff --git a/packages/voucherify/src/commerce-wrapper/get-cart.ts b/packages/voucherify/src/commerce-wrapper/get-cart.ts index 9932b97..d5f93aa 100644 --- a/packages/voucherify/src/commerce-wrapper/get-cart.ts +++ b/packages/voucherify/src/commerce-wrapper/get-cart.ts @@ -26,6 +26,8 @@ export const getCartFunction = cart, codes, }) - + console.log( + JSON.stringify({ cart, validationResult, promotionsResult }, null, 2) + ) return cartWithDiscount(cart, validationResult, promotionsResult) } diff --git a/packages/voucherify/src/validate-discounts.ts b/packages/voucherify/src/validate-discounts.ts index 1df20cf..b714a0f 100644 --- a/packages/voucherify/src/validate-discounts.ts +++ b/packages/voucherify/src/validate-discounts.ts @@ -47,6 +47,7 @@ export const validateCouponsAndPromotions = async ( ...getRedeemmablesForValidationFromPromotions(promotionsResult), ], order, + options: { expand: ['order'] }, }) return { promotionsResult, validationResult } From 1f044eb447c390090a2e88694937d7a87f7f3d00 Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Tue, 7 Nov 2023 12:15:36 +0100 Subject: [PATCH 12/13] Voucherify documentation --- .../integrations/promotions/_category_.json | 8 + .../integrations/promotions/voucherify.md | 89 ++++ docs/docusaurus.config.js | 6 + docs/package.json | 1 + docs/yarn.lock | 435 +++++++++++++++++- 5 files changed, 533 insertions(+), 6 deletions(-) create mode 100644 docs/docs/integrations/promotions/_category_.json create mode 100644 docs/docs/integrations/promotions/voucherify.md diff --git a/docs/docs/integrations/promotions/_category_.json b/docs/docs/integrations/promotions/_category_.json new file mode 100644 index 0000000..6d50d06 --- /dev/null +++ b/docs/docs/integrations/promotions/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Promotions", + "position": 7, + "link": { + "type": "generated-index", + "description": "Promotions" + } +} diff --git a/docs/docs/integrations/promotions/voucherify.md b/docs/docs/integrations/promotions/voucherify.md new file mode 100644 index 0000000..179116f --- /dev/null +++ b/docs/docs/integrations/promotions/voucherify.md @@ -0,0 +1,89 @@ + +# Voucherify Integration + +Voucherify is an API-first Promotions and Loyalty Engine that helps brands run personalised coupons, gift cards, auto-applied promotions, loyalty programs, and referral campaigns. This integration focuses on educating people about using Voucherify in composable commerce to enhance advanced promotions capabilities. + +## Integration architecture + +To extend the storefront by promotion capabilities, Voucherify integration: +1. wrap commerce-generic service extending server API by additional methods and data related to marketing promotions, +2. provides additional UI components that allow users to manage coupons and auto-applied promotions + +Voucherify commerce wrapper implements a standard checkout integration pattern, where for each cart update, the wrapper makes Validation (eligibility check, discount calculations) and Qualification (list of applicable promotions) requests to Voucherify REST API. Collected pieces of information extend cart data. + + + +```mermaid +sequenceDiagram + participant B as Browser + participant VCW as Commerce Wrapper + participant CGS as Commerce Generic + participant V as Voucherify API + B->>VCW: tRPC, CommerceServiceWithDiscounts interface extends CommerceService + activate VCW + VCW->>CGS: CommerceService interface (getCart, addCartItem etc) + activate CGS + CGS-->>VCW: Cart object + deactivate CGS + Note over VCW: Get cart vouchers codes from storage + VCW->>V: get available cart promotions + activate V + V-->>VCW: promotions + deactivate V + VCW->>V: validate promotions and vouchers codes + activate V + V-->>VCW: eligibility check and discount calculations results + deactivate V + Note over VCW: Extend cart by discounts details + VCW-->>B: CartWithDiscount object + deactivate VCW + +``` + +## Reference Files + +### Backend files + +- `packages/voucherify`: This package contains services used to wrap commerce-generic services and communicate to the Voucherify REST API. + +### React Components + +- `composable-ui/src/components/forms/coupon-form.tsx`: Form to manage coupons, gift and loyalties cards +- `composable-ui/src/components/cart/cart-promotions.tsx`: Display auto-applied cart promotions + +## Integrating Voucherify with Composable UI + +1. [Create a Voucherify account](https://app.voucherify.io/#/signup). +2. Retrieve your API keys from your Voucherify dashboard and set the following environment variables: + +:::caution + +Ensure you never expose your Voucherify API keys in the NEXT_PUBLIC_* environment variables or client-side code. Take the necessary steps to ensure that secret keys are never disclosed to the public. +::: + +```bash +VOUCHERIFY_API_URL=https://api.voucherify.io +VOUCHERIFY_APPLICATION_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx +VOUCHERIFY_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx +``` + +## Populating the Products Using the Script + +To configure product base promotions in Voucherify, propagate product definitions to your Voucherify account: + +1. Open the terminal and navigate to the `scripts` directory. +2. In the `scripts` directory, run the following command: + ``` + pnpm install + ``` +3. To set up Algolia, run the following command: + ``` + pnpm voucherify-setup + ``` + +For more information about the configurations, see the [Application Configuration](essentials/configuration.md) section. + +## Related Resources + +- [Application Configuration](essentials/configuration.md) +- [Mono-repository](essentials/monorepo.md) \ No newline at end of file diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 10b3144..dd22539 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -29,6 +29,12 @@ const config = { organizationName: 'facebook', // Usually your GitHub org/user name. projectName: 'docusaurus', // Usually your repo name. + // Mermaid + markdown: { + mermaid: true, + }, + themes: ['@docusaurus/theme-mermaid'], + // Even if you don't use internalization, you can use this field to set useful // metadata like html lang. For example, if your site is Chinese, you may want // to replace "en" with "zh-Hans". diff --git a/docs/package.json b/docs/package.json index dfe3dff..f028881 100644 --- a/docs/package.json +++ b/docs/package.json @@ -17,6 +17,7 @@ "dependencies": { "@docusaurus/core": "2.4.1", "@docusaurus/preset-classic": "2.4.1", + "@docusaurus/theme-mermaid": "2.4.1", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "dotenv": "^16.0.3", diff --git a/docs/yarn.lock b/docs/yarn.lock index 491cf9a..0c0e283 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -1235,6 +1235,11 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@braintree/sanitize-url@^6.0.0": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" + integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -1587,6 +1592,20 @@ use-sync-external-store "^1.2.0" utility-types "^3.10.0" +"@docusaurus/theme-mermaid@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-2.4.1.tgz#6bf644f9f7ef3db0e938b484f510d6d80d601419" + integrity sha512-cM0ImKIqZfjmlaC+uAjep39kNBvb1bjz429QBHGs32maob4+UnRzVPPpCUCltyPVb4xjG5h1Tyq4pHzhtIikqA== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/module-type-aliases" "2.4.1" + "@docusaurus/theme-common" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" + "@mdx-js/react" "^1.6.22" + mermaid "^9.2.2" + tslib "^2.4.0" + "@docusaurus/theme-search-algolia@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.1.tgz#906bd2cca3fced0241985ef502c892f58ff380fc" @@ -3031,6 +3050,11 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== +commander@7, commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -3041,11 +3065,6 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - commander@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" @@ -3174,6 +3193,20 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cose-base@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a" + integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg== + dependencies: + layout-base "^1.0.0" + +cose-base@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-2.2.0.tgz#1c395c35b6e10bb83f9769ca8b817d614add5c01" + integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g== + dependencies: + layout-base "^2.0.0" + cosmiconfig@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" @@ -3371,6 +3404,285 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +cytoscape-cose-bilkent@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b" + integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ== + dependencies: + cose-base "^1.0.0" + +cytoscape-fcose@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz#e4d6f6490df4fab58ae9cea9e5c3ab8d7472f471" + integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ== + dependencies: + cose-base "^2.2.0" + +cytoscape@^3.23.0: + version "3.27.0" + resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.27.0.tgz#5141cd694570807c91075b609181bce102e0bb88" + integrity sha512-pPZJilfX9BxESwujODz5pydeGi+FBrXq1rcaB1mfhFXXFJ9GjE6CNndAk+8jPzoXGD+16LtSS4xlYEIUiW4Abg== + dependencies: + heap "^0.2.6" + lodash "^4.17.21" + +"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +d3-axis@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" + integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== + +d3-brush@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" + integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "3" + d3-transition "3" + +d3-chord@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" + integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== + dependencies: + d3-path "1 - 3" + +"d3-color@1 - 3", d3-color@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-contour@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc" + integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== + dependencies: + d3-array "^3.2.0" + +d3-delaunay@6: + version "6.0.4" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" + integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== + dependencies: + delaunator "5" + +"d3-dispatch@1 - 3", d3-dispatch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-dsv@1 - 3", d3-dsv@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" + integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== + dependencies: + commander "7" + iconv-lite "0.6" + rw "1" + +"d3-ease@1 - 3", d3-ease@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +d3-fetch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" + integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== + dependencies: + d3-dsv "1 - 3" + +d3-force@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" + integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== + dependencies: + d3-dispatch "1 - 3" + d3-quadtree "1 - 3" + d3-timer "1 - 3" + +"d3-format@1 - 3", d3-format@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +d3-geo@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e" + integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA== + dependencies: + d3-array "2.5.0 - 3" + +d3-hierarchy@3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" + integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== + +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-polygon@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" + integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== + +"d3-quadtree@1 - 3", d3-quadtree@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" + integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== + +d3-random@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" + integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== + +d3-scale-chromatic@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a" + integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g== + dependencies: + d3-color "1 - 3" + d3-interpolate "1 - 3" + +d3-scale@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +"d3-selection@2 - 3", d3-selection@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +d3-shape@3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4", d3-time-format@4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +"d3-timer@1 - 3", d3-timer@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3", d3-transition@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + +d3@^7.4.0, d3@^7.8.2: + version "7.8.5" + resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.5.tgz#fde4b760d4486cdb6f0cc8e2cbff318af844635c" + integrity sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA== + dependencies: + d3-array "3" + d3-axis "3" + d3-brush "3" + d3-chord "3" + d3-color "3" + d3-contour "4" + d3-delaunay "6" + d3-dispatch "3" + d3-drag "3" + d3-dsv "3" + d3-ease "3" + d3-fetch "3" + d3-force "3" + d3-format "3" + d3-geo "3" + d3-hierarchy "3" + d3-interpolate "3" + d3-path "3" + d3-polygon "3" + d3-quadtree "3" + d3-random "3" + d3-scale "4" + d3-scale-chromatic "3" + d3-selection "3" + d3-shape "3" + d3-time "3" + d3-time-format "4" + d3-timer "3" + d3-transition "3" + d3-zoom "3" + +dagre-d3-es@7.0.9: + version "7.0.9" + resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz#aca12fccd9d09955a4430029ba72ee6934542a8d" + integrity sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w== + dependencies: + d3 "^7.8.2" + lodash-es "^4.17.21" + +dayjs@^1.11.7: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + debug@2.6.9, debug@^2.6.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3441,6 +3753,13 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" +delaunator@5: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b" + integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw== + dependencies: + robust-predicates "^3.0.0" + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -3547,6 +3866,11 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" +dompurify@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.3.tgz#f4133af0e6a50297fc8874e2eaedc13a3c308c03" + integrity sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ== + domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" @@ -3610,6 +3934,11 @@ electron-to-chromium@^1.4.411: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.425.tgz#399df13091b836d28283a545c25c8e4d9da86da8" integrity sha512-wv1NufHxu11zfDbY4fglYQApMswleE9FL/DSeyOyauVXDZ+Kco96JK/tPfBUaDqfRarYp2WH2hJ/5UnVywp9Jg== +elkjs@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e" + integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -4331,6 +4660,11 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +heap@^0.2.6: + version "0.2.7" + resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc" + integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg== + history@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" @@ -4487,6 +4821,13 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.6: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" @@ -4570,6 +4911,11 @@ inline-style-parser@0.1.1: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -4908,6 +5254,11 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" +khroma@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.1.0.tgz#45f2ce94ce231a437cf5b63c2e886e6eb42bbbb1" + integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw== + kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -4938,6 +5289,16 @@ launch-editor@^2.6.0: picocolors "^1.0.0" shell-quote "^1.7.3" +layout-base@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2" + integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg== + +layout-base@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285" + integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg== + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -4994,6 +5355,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.curry@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" @@ -5144,6 +5510,28 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +mermaid@^9.2.2: + version "9.4.3" + resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-9.4.3.tgz#62cf210c246b74972ea98c19837519b6f03427f2" + integrity sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw== + dependencies: + "@braintree/sanitize-url" "^6.0.0" + cytoscape "^3.23.0" + cytoscape-cose-bilkent "^4.1.0" + cytoscape-fcose "^2.1.0" + d3 "^7.4.0" + dagre-d3-es "7.0.9" + dayjs "^1.11.7" + dompurify "2.4.3" + elkjs "^0.8.2" + khroma "^2.0.0" + lodash-es "^4.17.21" + non-layered-tidy-tree-layout "^2.0.2" + stylis "^4.1.2" + ts-dedent "^2.2.0" + uuid "^9.0.0" + web-worker "^1.2.0" + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -5295,6 +5683,11 @@ node-releases@^2.0.12: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039" integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== +non-layered-tidy-tree-layout@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" + integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -6464,6 +6857,11 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +robust-predicates@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" + integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== + rtl-detect@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.4.tgz#40ae0ea7302a150b96bc75af7d749607392ecac6" @@ -6486,6 +6884,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + rxjs@^7.5.4: version "7.8.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" @@ -6503,7 +6906,7 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -6951,6 +7354,11 @@ stylehacks@^5.1.1: browserslist "^4.21.4" postcss-selector-parser "^6.0.4" +stylis@^4.1.2: + version "4.3.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.0.tgz#abe305a669fc3d8777e10eefcfc73ad861c5588c" + integrity sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ== + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -7093,6 +7501,11 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +ts-dedent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" + integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== + tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: version "2.5.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" @@ -7361,6 +7774,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + value-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" @@ -7425,6 +7843,11 @@ web-namespaces@^1.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== +web-worker@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da" + integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From dcdb4ec7077b5152b03178af885822729106ed24 Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Tue, 7 Nov 2023 17:04:38 +0100 Subject: [PATCH 13/13] add to documentation information about required partial redeem --- docs/docs/integrations/promotions/voucherify.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docs/integrations/promotions/voucherify.md b/docs/docs/integrations/promotions/voucherify.md index 179116f..ba76c04 100644 --- a/docs/docs/integrations/promotions/voucherify.md +++ b/docs/docs/integrations/promotions/voucherify.md @@ -54,7 +54,8 @@ sequenceDiagram ## Integrating Voucherify with Composable UI 1. [Create a Voucherify account](https://app.voucherify.io/#/signup). -2. Retrieve your API keys from your Voucherify dashboard and set the following environment variables: +2. In Voucherify Dashbioard, [set Discount Application Rule to "Partial"](https://support.voucherify.io/article/604-stacking-rules#application-rules) +3. Retrieve your API keys from your Voucherify dashboard and set the following environment variables: :::caution