From 0461d97640f3d53b174196334d0bcc7b346fe3eb Mon Sep 17 00:00:00 2001 From: Marcin Slezak Date: Mon, 16 Oct 2023 17:11:17 +0200 Subject: [PATCH 01/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] - 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 d026c93c9e91d550edd16221f2f20f00ee248297 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 7 Nov 2023 09:20:27 +0100 Subject: [PATCH 12/49] fix naming --- ...validation.ts => get-redeemables-for-validation.ts} | 4 ++-- packages/voucherify/src/validate-discounts.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename packages/voucherify/data/{get-redeemmables-for-validation.ts => get-redeemables-for-validation.ts} (71%) diff --git a/packages/voucherify/data/get-redeemmables-for-validation.ts b/packages/voucherify/data/get-redeemables-for-validation.ts similarity index 71% rename from packages/voucherify/data/get-redeemmables-for-validation.ts rename to packages/voucherify/data/get-redeemables-for-validation.ts index 2b845a6..9902bb0 100644 --- a/packages/voucherify/data/get-redeemmables-for-validation.ts +++ b/packages/voucherify/data/get-redeemables-for-validation.ts @@ -1,12 +1,12 @@ import { PromotionsValidateResponse } from '@voucherify/sdk' -export const getRedeemmablesForValidation = (couponCodes: string[]) => +export const getRedeemablesForValidation = (couponCodes: string[]) => couponCodes.map((couponCode) => ({ id: couponCode, object: 'voucher' as const, })) -export const getRedeemmablesForValidationFromPromotions = ( +export const getRedeemablesForValidationFromPromotions = ( promotionResult: PromotionsValidateResponse ) => promotionResult.promotions?.map((promotion) => ({ diff --git a/packages/voucherify/src/validate-discounts.ts b/packages/voucherify/src/validate-discounts.ts index b714a0f..132e0de 100644 --- a/packages/voucherify/src/validate-discounts.ts +++ b/packages/voucherify/src/validate-discounts.ts @@ -6,9 +6,9 @@ import { VoucherifyServerSide, } from '@voucherify/sdk' import { - getRedeemmablesForValidation, - getRedeemmablesForValidationFromPromotions, -} from '../data/get-redeemmables-for-validation' + getRedeemablesForValidation, + getRedeemablesForValidationFromPromotions, +} from '../data/get-redeemables-for-validation' import { cartToVoucherifyOrder } from './cart-to-voucherify-order' type ValidateDiscountsParam = { @@ -43,8 +43,8 @@ export const validateCouponsAndPromotions = async ( const validationResult = await voucherify.validations.validateStackable({ redeemables: [ - ...getRedeemmablesForValidation(codes), - ...getRedeemmablesForValidationFromPromotions(promotionsResult), + ...getRedeemablesForValidation(codes), + ...getRedeemablesForValidationFromPromotions(promotionsResult), ], order, options: { expand: ['order'] }, From 6c9b2354eedb1de7bbff581b8e8303918b97027b Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:29:04 +0100 Subject: [PATCH 13/49] add discount info to sidebar and checkout --- .../cart/cart-drawer/cart-drawer-footer.tsx | 6 ++- .../cart/cart-drawer/cart-drawer-summary.tsx | 49 +++++++++++++++---- .../cart/cart-drawer/cart-drawer.tsx | 15 +++++- .../src/components/cart/cart-summary.tsx | 2 +- .../src/components/checkout/order-summary.tsx | 30 ++++++++++-- .../src/components/checkout/order-totals.tsx | 11 +++-- 6 files changed, 90 insertions(+), 23 deletions(-) diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx index bc401f9..bc0208f 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx @@ -11,12 +11,14 @@ import { Text, VStack, } from '@chakra-ui/react' +import { CartSummaryProps } from '../cart-summary' -export const CartDrawerFooter = () => { +export const CartDrawerFooter = ({ cartData }: CartSummaryProps) => { const router = useRouter() const { cartDrawer } = useComposable() const { cart } = useCart() const intl = useIntl() + const _cartData = cartData ?? cart return ( @@ -33,7 +35,7 @@ export const CartDrawerFooter = () => { diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx index 074590f..2a0d2ce 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx @@ -3,50 +3,53 @@ import { useIntl } from 'react-intl' import { useCart } from 'hooks' import { CartDrawerSummaryItem } from './cart-drawer-summary-item' import { Price } from '../../price' +import { CartSummaryItem } from '../cart-summary-item' +import { CartSummaryProps } from '../cart-summary' -export const CartDrawerSummary = () => { +export const CartDrawerSummary = ({ cartData }: CartSummaryProps) => { const { cart } = useCart() const intl = useIntl() + const _cartData = cartData ?? cart return ( - {cart.summary?.subtotalPrice && ( + {_cartData.summary?.subtotalPrice && ( )} - {cart.summary?.taxes && ( + {_cartData.summary?.taxes && ( )} - {cart.summary?.shipping && ( + {_cartData.summary?.shipping && ( )} - {cart.summary?.totalPrice && ( + {_cartData.summary?.totalPrice && ( <> { )} + + {_cartData.summary?.totalDiscountAmount && ( + + + + )} + + {_cartData.summary?.grandPrice && ( + <> + + + {intl.formatMessage({ id: 'cart.summary.grandPrice' })} + + + + + + )} ) } diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx index 4d9984d..183894f 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx @@ -17,13 +17,15 @@ import { Text, } from '@chakra-ui/react' import { APP_CONFIG } from '../../../utils/constants' -import { CartLoadingState } from '../.' +import { CartLoadingState, CartSummaryProps } from '../.' import { CartDrawerFooter } from './cart-drawer-footer' import { CartDrawerSummary } from './cart-drawer-summary' import { CartDrawerEmptyState } from './cart-drawer-empty-state' import { HorizontalProductCard } from '@composable/ui' +import { CouponForm } from '../../forms/coupon-form' +import { CartPromotions } from '../cart-promotions' -export const CartDrawer = () => { +export const CartDrawer = ({ cartData }: CartSummaryProps) => { const intl = useIntl() const toast = useToast() const router = useRouter() @@ -38,6 +40,8 @@ export const CartDrawer = () => { }, }) + const _cartData = cartData ?? cart + const title = intl.formatMessage( { id: 'cart.drawer.titleCount' }, { count: cart.quantity } @@ -47,6 +51,11 @@ export const CartDrawer = () => { style: 'currency', } + const promotions = + _cartData.redeemables?.filter( + (redeemable) => redeemable.object === 'promotion_tier' + ) || [] + useEffect(() => { router.events.on('routeChangeStart', cartDrawer.onClose) return () => { @@ -148,6 +157,8 @@ export const CartDrawer = () => { ) })} + + )} diff --git a/composable-ui/src/components/cart/cart-summary.tsx b/composable-ui/src/components/cart/cart-summary.tsx index be38b4b..2d971f2 100644 --- a/composable-ui/src/components/cart/cart-summary.tsx +++ b/composable-ui/src/components/cart/cart-summary.tsx @@ -15,7 +15,7 @@ import { import { CartSummaryItem } from '.' import { CartPromotions } from './cart-promotions' -interface CartSummaryProps { +export interface CartSummaryProps { rootProps?: StackProps renderCheckoutButton?: boolean cartData?: CartData diff --git a/composable-ui/src/components/checkout/order-summary.tsx b/composable-ui/src/components/checkout/order-summary.tsx index 77545b1..7cacb0b 100644 --- a/composable-ui/src/components/checkout/order-summary.tsx +++ b/composable-ui/src/components/checkout/order-summary.tsx @@ -10,20 +10,25 @@ import { Stack, Text, } from '@chakra-ui/react' -import { useCart, useCheckout } from 'hooks' +import { CartData, useCart, useCheckout } from 'hooks' import { FormatNumberOptions, useIntl } from 'react-intl' import { APP_CONFIG } from '../../utils/constants' import { OrderTotals } from './order-totals' import { ProductsList } from './products-list' +import { CartPromotions } from '../cart/cart-promotions' +import { CouponForm } from '../forms/coupon-form' +import { CartSummaryProps } from '../cart' export interface CheckoutSidebarProps { itemsBoxProps?: AccordionProps showTitle?: boolean + cartData?: CartData } export const OrderSummary = ({ itemsBoxProps, showTitle = true, + cartData, }: CheckoutSidebarProps) => { const intl = useIntl() const { cart } = useCart() @@ -35,6 +40,13 @@ export const OrderSummary = ({ style: 'currency', } + const _cartData = cartData ?? cart + + const promotions = + _cartData.redeemables?.filter( + (redeemable) => redeemable.object === 'promotion_tier' + ) || [] + const numItems = _cart.items?.reduce((acc, cur) => acc + cur.quantity, 0) return ( @@ -87,10 +99,11 @@ export const OrderSummary = ({ - + + diff --git a/composable-ui/src/components/checkout/order-totals.tsx b/composable-ui/src/components/checkout/order-totals.tsx index 18bae1f..a30bf1b 100644 --- a/composable-ui/src/components/checkout/order-totals.tsx +++ b/composable-ui/src/components/checkout/order-totals.tsx @@ -9,6 +9,8 @@ interface OrderTotalsProps { discount?: string totalTitle?: string total: string + totalDiscountAmountTitle?: string + totalDiscountAmount?: string } export const OrderTotals = ({ @@ -16,9 +18,10 @@ export const OrderTotals = ({ deliveryTitle, delivery, tax, - discount, totalTitle, total, + totalDiscountAmountTitle, + totalDiscountAmount, }: OrderTotalsProps) => { const intl = useIntl() @@ -47,11 +50,11 @@ export const OrderTotals = ({ label={intl.formatMessage({ id: 'cart.summary.tax' })} value={tax} /> - {discount && ( + {totalDiscountAmount && ( )} From af4245233a3a19fe8207cf33ff141715f8dd3c0f Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:41:59 +0100 Subject: [PATCH 14/49] wrapped order with discount --- .../commerce-service-with-discounts.ts | 4 ++ .../src/voucherify/order-with-discounts.ts | 19 ++++++++++ .../voucherify/data/order-with-discount.ts | 37 +++++++++++++++++++ .../src/commerce-wrapper/create-order.ts | 34 +++++++++++++++++ .../voucherify/src/commerce-wrapper/index.ts | 2 + 5 files changed, 96 insertions(+) create mode 100644 packages/types/src/voucherify/order-with-discounts.ts create mode 100644 packages/voucherify/data/order-with-discount.ts create mode 100644 packages/voucherify/src/commerce-wrapper/create-order.ts diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts index 48bf6c4..55c3afd 100644 --- a/packages/types/src/voucherify/commerce-service-with-discounts.ts +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -1,5 +1,6 @@ import { CommerceService, Cart, CartItem } from '../commerce' import { CartWithDiscounts } from './cart-with-discounts' +import { OrderWithDiscounts } from './order-with-discounts' export interface CommerceServiceWithDiscounts extends CommerceService { // Extend exisiting commerce service methods to return cart with applied discount detasils @@ -28,4 +29,7 @@ export interface CommerceServiceWithDiscounts extends CommerceService { coupon: string cartId: string }): Promise + createOrder( + ...params: Parameters + ): Promise } diff --git a/packages/types/src/voucherify/order-with-discounts.ts b/packages/types/src/voucherify/order-with-discounts.ts new file mode 100644 index 0000000..c39d48d --- /dev/null +++ b/packages/types/src/voucherify/order-with-discounts.ts @@ -0,0 +1,19 @@ +import { Order } from '../commerce' + +export type OrderWithDiscounts = Order & { + orderType: 'OrderWithDiscounts' + 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 + } +} diff --git a/packages/voucherify/data/order-with-discount.ts b/packages/voucherify/data/order-with-discount.ts new file mode 100644 index 0000000..3685c68 --- /dev/null +++ b/packages/voucherify/data/order-with-discount.ts @@ -0,0 +1,37 @@ +import { OrderWithDiscounts } from '@composable/types/src/voucherify/order-with-discounts' +import { centToString, toCent } from '../src/to-cent' +import { Cart, Order } from '@composable/types' +import { ValidationValidateStackableResponse } from '@voucherify/sdk' + +export const orderWithDiscount = ( + order: Order | null, + cart: Cart, + validationResponse: ValidationValidateStackableResponse | false +): OrderWithDiscounts | null => { + 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 + ) + if (!order) { + return null + } + return { + ...order, + orderType: 'OrderWithDiscounts', + summary: { + ...cart.summary, + discountAmount, + totalDiscountAmount, + grandPrice, + }, + } +} diff --git a/packages/voucherify/src/commerce-wrapper/create-order.ts b/packages/voucherify/src/commerce-wrapper/create-order.ts new file mode 100644 index 0000000..d32fd15 --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/create-order.ts @@ -0,0 +1,34 @@ +import { CommerceService } from '@composable/types' +import { VoucherifyServerSide } from '@voucherify/sdk' +import { OrderWithDiscounts } from '@composable/types/src/voucherify/order-with-discounts' +import { orderWithDiscount } from '../../data/order-with-discount' +import { validateCouponsAndPromotions } from '../validate-discounts' +import { getCartDiscounts } from '../../data/persit' + +export const createOrderFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ( + getCartProps: Parameters, + getOrderProps: Parameters + ): Promise => { + const cart = await commerceService.getCart(...getCartProps) + const order = await commerceService.getOrder(...getOrderProps) + + const codes = await getCartDiscounts(getCartProps[0]?.cartId) + + if (!cart) { + return null + } + + const { validationResult, promotionsResult } = + await validateCouponsAndPromotions({ + voucherify, + cart, + codes, + }) + + return orderWithDiscount(order, cart, validationResult) + } diff --git a/packages/voucherify/src/commerce-wrapper/index.ts b/packages/voucherify/src/commerce-wrapper/index.ts index 5c17a14..e839e72 100644 --- a/packages/voucherify/src/commerce-wrapper/index.ts +++ b/packages/voucherify/src/commerce-wrapper/index.ts @@ -10,6 +10,7 @@ import { deleteCartItemFunction } from './delete-cart-item' import { updateCartItemFunction } from './update-cart-item' import { addCouponFunction } from './add-coupon' import { deleteCouponFunction } from './delete-coupon' +import { createOrderFunction } from './create-order' if ( !process.env.VOUCHERIFY_APPLICATION_ID || @@ -40,5 +41,6 @@ export const commerceWithDiscount = ( updateCartItem: updateCartItemFunction(commerceService, voucherify), addCoupon: addCouponFunction(commerceService, voucherify), deleteCoupon: deleteCouponFunction(commerceService, voucherify), + createOrder: createOrderFunction(commerceService, voucherify), } } From aef8301a44f421aca61ddc87f1bff49efadeb530 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:25:26 +0100 Subject: [PATCH 15/49] Order With Discounts --- packages/commerce-generic/src/data/persit.ts | 5 +- .../commerce-service-with-discounts.ts | 14 +++-- .../src/voucherify/order-with-discounts.ts | 8 +-- .../src/commerce-wrapper/create-order.ts | 55 +++++++++++++++---- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/packages/commerce-generic/src/data/persit.ts b/packages/commerce-generic/src/data/persit.ts index 9dd7eb1..c985f03 100644 --- a/packages/commerce-generic/src/data/persit.ts +++ b/packages/commerce-generic/src/data/persit.ts @@ -13,7 +13,10 @@ export const getOrder = async (orderId: string): Promise => { return storage.getItem(`order-${orderId}`) } -export const saveOrder = async (order: Order) => { +export const saveOrder = async (order: Order | null) => { + if (!order) { + return null + } await storage.setItem(`order-${order.id}`, order) return order } diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts index 55c3afd..7b07aeb 100644 --- a/packages/types/src/voucherify/commerce-service-with-discounts.ts +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -1,4 +1,10 @@ -import { CommerceService, Cart, CartItem } from '../commerce' +import { + CommerceService, + Cart, + CartItem, + CheckoutInput, + Order, +} from '../commerce' import { CartWithDiscounts } from './cart-with-discounts' import { OrderWithDiscounts } from './order-with-discounts' @@ -29,7 +35,7 @@ export interface CommerceServiceWithDiscounts extends CommerceService { coupon: string cartId: string }): Promise - createOrder( - ...params: Parameters - ): Promise + createOrder(params: { + checkout: CheckoutInput + }): Promise } diff --git a/packages/types/src/voucherify/order-with-discounts.ts b/packages/types/src/voucherify/order-with-discounts.ts index c39d48d..2f0bc34 100644 --- a/packages/types/src/voucherify/order-with-discounts.ts +++ b/packages/types/src/voucherify/order-with-discounts.ts @@ -1,19 +1,19 @@ import { Order } from '../commerce' export type OrderWithDiscounts = Order & { - orderType: 'OrderWithDiscounts' + orderType?: 'OrderWithDiscounts' summary: { /** * Sum of all order-level discounts applied to the order. */ - discountAmount: string + discountAmount?: string /** * Sum of all order-level AND all product-specific discounts applied to the order. */ - totalDiscountAmount: string + totalDiscountAmount?: string /** * Order amount after applying all the discounts. */ - grandPrice: string + grandPrice?: string } } diff --git a/packages/voucherify/src/commerce-wrapper/create-order.ts b/packages/voucherify/src/commerce-wrapper/create-order.ts index d32fd15..71426b9 100644 --- a/packages/voucherify/src/commerce-wrapper/create-order.ts +++ b/packages/voucherify/src/commerce-wrapper/create-order.ts @@ -1,23 +1,24 @@ -import { CommerceService } from '@composable/types' +import { Cart, CheckoutInput, CommerceService, Order } from '@composable/types' import { VoucherifyServerSide } from '@voucherify/sdk' -import { OrderWithDiscounts } from '@composable/types/src/voucherify/order-with-discounts' import { orderWithDiscount } from '../../data/order-with-discount' import { validateCouponsAndPromotions } from '../validate-discounts' import { getCartDiscounts } from '../../data/persit' +import { randomUUID } from 'crypto' +import shippingMethods from '../../../commerce-generic/src/data/shipping-methods.json' +import { saveOrder } from '../../../commerce-generic/src/data/persit' export const createOrderFunction = ( commerceService: CommerceService, voucherify: ReturnType ) => - async ( - getCartProps: Parameters, - getOrderProps: Parameters - ): Promise => { - const cart = await commerceService.getCart(...getCartProps) - const order = await commerceService.getOrder(...getOrderProps) + async (...props: Parameters) => { + const cartId = props[0].checkout.cartId + const cart = await commerceService.getCart({ + cartId: cartId, + }) - const codes = await getCartDiscounts(getCartProps[0]?.cartId) + const codes = await getCartDiscounts(cartId) if (!cart) { return null @@ -30,5 +31,39 @@ export const createOrderFunction = codes, }) - return orderWithDiscount(order, cart, validationResult) + const generateOrderFromCart = ( + cart: Cart, + checkoutInput: CheckoutInput + ): Order => { + return { + id: randomUUID(), + status: 'complete', + payment: 'unpaid', + shipping: 'unfulfilled', + customer: { + email: checkoutInput.customer.email, + }, + shipping_address: { + phone_number: '', + city: '', + ...checkoutInput.shipping_address, + }, + billing_address: { + phone_number: '', + city: '', + ...checkoutInput.billing_address, + }, + shipping_method: shippingMethods[0], + created_at: Date.now(), + items: cart.items, + summary: cart.summary, + } + } + + const orderWithDiscounts = orderWithDiscount( + generateOrderFromCart(cart, props[0].checkout), + cart, + validationResult + ) + return saveOrder(orderWithDiscounts) } From abb973b5564ea20d4a84ab822a8f07f003f67930 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:35:42 +0100 Subject: [PATCH 16/49] send grandPrice to Stripe --- composable-ui/src/server/api/routers/stripe.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composable-ui/src/server/api/routers/stripe.ts b/composable-ui/src/server/api/routers/stripe.ts index e2187e9..639f3c1 100644 --- a/composable-ui/src/server/api/routers/stripe.ts +++ b/composable-ui/src/server/api/routers/stripe.ts @@ -59,7 +59,11 @@ export const stripeRouter = createTRPCRouter({ const cart = await commerce.getCart({ cartId: input.cartId }) return await stripeProvider.createPaymentIntent({ amount: parseInt( - (parseFloat(cart?.summary.totalPrice ?? '0') * 100).toString() + ( + parseFloat( + cart?.summary.grandPrice ?? cart?.summary.totalPrice ?? '0' + ) * 100 + ).toString() ), currency: APP_CONFIG.CURRENCY_CODE, customer: input.customerId, From e7f33d1673f81afc09a6f1bfdeb09f037a05132c Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Thu, 9 Nov 2023 08:47:57 +0100 Subject: [PATCH 17/49] Display discounts properly --- composable-ui/src/components/checkout/order-summary.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composable-ui/src/components/checkout/order-summary.tsx b/composable-ui/src/components/checkout/order-summary.tsx index 7cacb0b..951e596 100644 --- a/composable-ui/src/components/checkout/order-summary.tsx +++ b/composable-ui/src/components/checkout/order-summary.tsx @@ -122,10 +122,10 @@ export const OrderSummary = ({ currencyFormatConfig )} totalDiscountAmountTitle={intl.formatMessage({ - id: '_cartData.summary.totalDiscountAmount', + id: 'cart.summary.totalDiscountAmount', })} totalDiscountAmount={intl.formatNumber( - parseFloat(_cartData?.summary?.totalDiscountAmount ?? '0'), + parseFloat(_cartData?.summary?.totalDiscountAmount || '0'), currencyFormatConfig )} /> From 5244d40f833b5ab4ecebd5bbec34f4199204f905 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:12:20 +0100 Subject: [PATCH 18/49] Add redemption to checkout --- composable-ui/src/hooks/use-checkout.tsx | 21 +++++++++++ .../commerce/procedures/cart/redeem-coupon.ts | 14 +++++++ .../commerce-service-with-discounts.ts | 12 ++++-- .../data/get-redeemables-for-validation.ts | 6 +++ .../voucherify/data/order-with-discount.ts | 17 +++++---- .../voucherify/src/commerce-wrapper/index.ts | 2 + .../is-redemption-succeeded.ts | 19 ++++++++++ .../src/commerce-wrapper/redeem-coupons.ts | 32 ++++++++++++++++ packages/voucherify/src/redeem-coupons.ts | 37 +++++++++++++++++++ 9 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 composable-ui/src/server/api/routers/commerce/procedures/cart/redeem-coupon.ts create mode 100644 packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts create mode 100644 packages/voucherify/src/commerce-wrapper/redeem-coupons.ts create mode 100644 packages/voucherify/src/redeem-coupons.ts diff --git a/composable-ui/src/hooks/use-checkout.tsx b/composable-ui/src/hooks/use-checkout.tsx index a597f82..84f4a80 100644 --- a/composable-ui/src/hooks/use-checkout.tsx +++ b/composable-ui/src/hooks/use-checkout.tsx @@ -23,6 +23,17 @@ export const useCheckout = () => { ...publicContext } = context + /** + * Redeem Coupon Mutation + */ + const redeemCouponMutation = useMutation(async (couponrCode) => { + const response = await client.commerce.redeemVoucher.mutate({}) + if (!response.ok) { + throw new Error('Failed to redeem.') + } + return response.json() + }) + /** * Place Order Mutation */ @@ -34,8 +45,18 @@ export const useCheckout = () => { let __checkoutResponse = context.response.checkout let redirectUrl + const couponCode = context.cartSnapshot.couponApplied if (!__checkoutResponse) { try { + if (couponCode) { + try { + redemptionResponse = await redeemCouponMutation.mutateAsync( + couponCode + ) + } catch (error) { + throw new Error('Failed to redeem coupon.') + } + } const params = { ...state, } diff --git a/composable-ui/src/server/api/routers/commerce/procedures/cart/redeem-coupon.ts b/composable-ui/src/server/api/routers/commerce/procedures/cart/redeem-coupon.ts new file mode 100644 index 0000000..b901325 --- /dev/null +++ b/composable-ui/src/server/api/routers/commerce/procedures/cart/redeem-coupon.ts @@ -0,0 +1,14 @@ +import { protectedProcedure } from '../../../../trpc' +import { z } from 'zod' +import { commerce } from '../../../../../data-source' + +export const redeemCoupon = protectedProcedure + .input( + z.object({ + cartId: z.string(), + coupon: z.string(), + }) + ) + .mutation(async ({ input }) => { + return await commerce.redeemCoupon({ ...input }) + }) diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts index 7b07aeb..d0991d1 100644 --- a/packages/types/src/voucherify/commerce-service-with-discounts.ts +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -9,7 +9,7 @@ import { CartWithDiscounts } from './cart-with-discounts' import { OrderWithDiscounts } from './order-with-discounts' export interface CommerceServiceWithDiscounts extends CommerceService { - // Extend exisiting commerce service methods to return cart with applied discount detasils + // Extend existing commerce service methods to return cart with applied discount details addCartItem( ...params: Parameters @@ -24,6 +24,9 @@ export interface CommerceServiceWithDiscounts extends CommerceService { updateCartItem( ...params: Parameters ): Promise + createOrder(params: { + checkout: CheckoutInput + }): Promise // Additional commerce endpoints to manage applied coupons @@ -35,7 +38,8 @@ export interface CommerceServiceWithDiscounts extends CommerceService { coupon: string cartId: string }): Promise - createOrder(params: { - checkout: CheckoutInput - }): Promise + redeemCoupons(props: { + coupons: string[] + cartId: string + }): Promise<{ result: boolean }> } diff --git a/packages/voucherify/data/get-redeemables-for-validation.ts b/packages/voucherify/data/get-redeemables-for-validation.ts index 9902bb0..af230c8 100644 --- a/packages/voucherify/data/get-redeemables-for-validation.ts +++ b/packages/voucherify/data/get-redeemables-for-validation.ts @@ -6,6 +6,12 @@ export const getRedeemablesForValidation = (couponCodes: string[]) => object: 'voucher' as const, })) +export const getRedeemablesForRedemption = (couponCodes: string[]) => + couponCodes.map((couponCode) => ({ + id: couponCode, + object: 'voucher' as const, + })) + export const getRedeemablesForValidationFromPromotions = ( promotionResult: PromotionsValidateResponse ) => diff --git a/packages/voucherify/data/order-with-discount.ts b/packages/voucherify/data/order-with-discount.ts index 3685c68..7fb72db 100644 --- a/packages/voucherify/data/order-with-discount.ts +++ b/packages/voucherify/data/order-with-discount.ts @@ -1,24 +1,27 @@ import { OrderWithDiscounts } from '@composable/types/src/voucherify/order-with-discounts' import { centToString, toCent } from '../src/to-cent' import { Cart, Order } from '@composable/types' -import { ValidationValidateStackableResponse } from '@voucherify/sdk' +import { + RedemptionsRedeemStackableResponse, + ValidationValidateStackableResponse, +} from '@voucherify/sdk' export const orderWithDiscount = ( order: Order | null, cart: Cart, - validationResponse: ValidationValidateStackableResponse | false + redemptionResponse: RedemptionsRedeemStackableResponse | false ): OrderWithDiscounts | null => { const discountAmount = centToString( - validationResponse ? validationResponse.order?.discount_amount : 0 + redemptionResponse ? redemptionResponse.order?.discount_amount : 0 ) const grandPrice = centToString( - validationResponse - ? validationResponse.order?.total_amount + redemptionResponse + ? redemptionResponse.order?.total_amount : toCent(cart.summary.totalPrice) ) const totalDiscountAmount = centToString( - validationResponse - ? validationResponse.order?.total_applied_discount_amount + redemptionResponse + ? redemptionResponse.order?.total_applied_discount_amount : 0 ) if (!order) { diff --git a/packages/voucherify/src/commerce-wrapper/index.ts b/packages/voucherify/src/commerce-wrapper/index.ts index e839e72..87a446a 100644 --- a/packages/voucherify/src/commerce-wrapper/index.ts +++ b/packages/voucherify/src/commerce-wrapper/index.ts @@ -11,6 +11,7 @@ import { updateCartItemFunction } from './update-cart-item' import { addCouponFunction } from './add-coupon' import { deleteCouponFunction } from './delete-coupon' import { createOrderFunction } from './create-order' +import { redeemCouponsFunction } from './redeem-coupons' if ( !process.env.VOUCHERIFY_APPLICATION_ID || @@ -41,6 +42,7 @@ export const commerceWithDiscount = ( updateCartItem: updateCartItemFunction(commerceService, voucherify), addCoupon: addCouponFunction(commerceService, voucherify), deleteCoupon: deleteCouponFunction(commerceService, voucherify), + redeemCoupons: redeemCouponsFunction(commerceService, voucherify), createOrder: createOrderFunction(commerceService, voucherify), } } diff --git a/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts b/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts new file mode 100644 index 0000000..80de78d --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts @@ -0,0 +1,19 @@ +import { RedeemCouponsResponse } from '../redeem-coupons' + +export const isRedemptionSucceeded = ( + redemptionResult: RedeemCouponsResponse, + coupon: string +): { isRedemptionSuccessful: boolean } => { + const isRedemptionSuccessful = + redemptionResult && + redemptionResult.redemptions.some( + (redemption) => + redemption.voucher.id === coupon && redemption.result === 'SUCCESS' + ) + + if (!isRedemptionSuccessful) { + throw new Error(`Redemption of code: ${coupon} failed.`) + } + + return { isRedemptionSuccessful } +} diff --git a/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts new file mode 100644 index 0000000..40123ab --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts @@ -0,0 +1,32 @@ +import { CommerceService } from '@composable/types' +import { VoucherifyServerSide } from '@voucherify/sdk' +import { redeemCoupons } from '../redeem-coupons' +import { isRedemptionSucceeded } from './is-redemption-succeeded' + +export const redeemCouponsFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ({ cartId, coupons }: { cartId: string; coupons: string[] }) => { + const cart = await commerceService.getCart({ cartId }) + + if (!cart) { + throw new Error( + `[voucherify][redeemCoupon] cart not found by id: ${cartId}` + ) + } + + const redemptionResult = await redeemCoupons({ + cart, + voucherify, + codes: coupons, + }) + + const redemptionsResults = coupons.map((coupon) => + isRedemptionSucceeded(redemptionResult, coupon) + ) + console.log('redemptionsResults', redemptionsResults) + + //return + } diff --git a/packages/voucherify/src/redeem-coupons.ts b/packages/voucherify/src/redeem-coupons.ts new file mode 100644 index 0000000..d4d6c53 --- /dev/null +++ b/packages/voucherify/src/redeem-coupons.ts @@ -0,0 +1,37 @@ +import { cartToVoucherifyOrder } from './cart-to-voucherify-order' +import { + getRedeemablesForRedemption, + getRedeemablesForValidation, +} from '../data/get-redeemables-for-validation' +import { Cart } from '@composable/types' +import { + RedemptionsRedeemStackableResponse, + StackableRedeemableResponse, + VoucherifyServerSide, +} from '@voucherify/sdk' + +type RedeemCouponsParam = { + cart: Cart + codes: string[] + voucherify: ReturnType +} + +export type RedeemCouponsResponse = + | false + | (RedemptionsRedeemStackableResponse & { + inapplicable_redeemables?: StackableRedeemableResponse[] + }) + +export const redeemCoupons = async ( + params: RedeemCouponsParam +): Promise => { + const { cart, codes, voucherify } = params + + const order = cartToVoucherifyOrder(cart) + + return await voucherify.redemptions.redeemStackable({ + redeemables: [...getRedeemablesForRedemption(codes)], + order, + options: { expand: ['order'] }, + }) +} From b2ada4c0d4531127e360f17cffec057a01432733 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Mon, 13 Nov 2023 13:19:48 +0100 Subject: [PATCH 19/49] use checkout - redeem --- composable-ui/src/hooks/use-checkout.tsx | 28 +++++++++++++------ .../commerce/procedures/cart/redeem-coupon.ts | 14 ---------- .../commerce/procedures/checkout/index.ts | 1 + .../procedures/checkout/redeem-coupons.ts | 14 ++++++++++ .../commerce-service-with-discounts.ts | 2 +- .../src/commerce-wrapper/redeem-coupons.ts | 6 ++-- 6 files changed, 39 insertions(+), 26 deletions(-) delete mode 100644 composable-ui/src/server/api/routers/commerce/procedures/cart/redeem-coupon.ts create mode 100644 composable-ui/src/server/api/routers/commerce/procedures/checkout/redeem-coupons.ts diff --git a/composable-ui/src/hooks/use-checkout.tsx b/composable-ui/src/hooks/use-checkout.tsx index 84f4a80..31f65c3 100644 --- a/composable-ui/src/hooks/use-checkout.tsx +++ b/composable-ui/src/hooks/use-checkout.tsx @@ -26,12 +26,18 @@ export const useCheckout = () => { /** * Redeem Coupon Mutation */ - const redeemCouponMutation = useMutation(async (couponrCode) => { - const response = await client.commerce.redeemVoucher.mutate({}) - if (!response.ok) { + const redeemCouponsMutation = useMutation(async (coupons: string[]) => { + if (!cart.id) { + return + } + const redeemedSuccessfully = await client.commerce.redeemCoupons.mutate({ + cartId: cart.id, + coupons, + }) + if (!redeemedSuccessfully) { throw new Error('Failed to redeem.') } - return response.json() + return redeemedSuccessfully }) /** @@ -45,14 +51,18 @@ export const useCheckout = () => { let __checkoutResponse = context.response.checkout let redirectUrl - const couponCode = context.cartSnapshot.couponApplied + + const coupons = context.cartSnapshot.redeemables + ?.filter((redeemable) => redeemable.status === 'APPLICABLE') + .map((redeemable) => { + return redeemable.id + }) if (!__checkoutResponse) { try { - if (couponCode) { + if (coupons) { try { - redemptionResponse = await redeemCouponMutation.mutateAsync( - couponCode - ) + await redeemCouponsMutation.mutateAsync(coupons) + console.log('Redeemed successfully.') } catch (error) { throw new Error('Failed to redeem coupon.') } diff --git a/composable-ui/src/server/api/routers/commerce/procedures/cart/redeem-coupon.ts b/composable-ui/src/server/api/routers/commerce/procedures/cart/redeem-coupon.ts deleted file mode 100644 index b901325..0000000 --- a/composable-ui/src/server/api/routers/commerce/procedures/cart/redeem-coupon.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { protectedProcedure } from '../../../../trpc' -import { z } from 'zod' -import { commerce } from '../../../../../data-source' - -export const redeemCoupon = protectedProcedure - .input( - z.object({ - cartId: z.string(), - coupon: z.string(), - }) - ) - .mutation(async ({ input }) => { - return await commerce.redeemCoupon({ ...input }) - }) diff --git a/composable-ui/src/server/api/routers/commerce/procedures/checkout/index.ts b/composable-ui/src/server/api/routers/commerce/procedures/checkout/index.ts index d3d8265..2ab23a5 100644 --- a/composable-ui/src/server/api/routers/commerce/procedures/checkout/index.ts +++ b/composable-ui/src/server/api/routers/commerce/procedures/checkout/index.ts @@ -1,3 +1,4 @@ export * from './create-order' export * from './get-order' export * from './get-shipping-methods' +export * from './redeem-coupons' diff --git a/composable-ui/src/server/api/routers/commerce/procedures/checkout/redeem-coupons.ts b/composable-ui/src/server/api/routers/commerce/procedures/checkout/redeem-coupons.ts new file mode 100644 index 0000000..905046e --- /dev/null +++ b/composable-ui/src/server/api/routers/commerce/procedures/checkout/redeem-coupons.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' +import { protectedProcedure } from 'server/api/trpc' +import { commerce } from 'server/data-source' + +export const redeemCoupons = protectedProcedure + .input( + z.object({ + cartId: z.string(), + coupons: z.array(z.string()), + }) + ) + .mutation(async ({ input }) => { + return await commerce.redeemCoupons({ ...input }) + }) diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts index d0991d1..fe488b7 100644 --- a/packages/types/src/voucherify/commerce-service-with-discounts.ts +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -39,7 +39,7 @@ export interface CommerceServiceWithDiscounts extends CommerceService { cartId: string }): Promise redeemCoupons(props: { - coupons: string[] cartId: string + coupons: string[] }): Promise<{ result: boolean }> } diff --git a/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts index 40123ab..4d86c16 100644 --- a/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts +++ b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts @@ -27,6 +27,8 @@ export const redeemCouponsFunction = isRedemptionSucceeded(redemptionResult, coupon) ) console.log('redemptionsResults', redemptionsResults) - - //return + const response = { + result: true, + } + return response } From dc616354e735b695c92ae9861747604f3680bf70 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Mon, 13 Nov 2023 13:29:08 +0100 Subject: [PATCH 20/49] Check whether the redemptions were successful --- .../commerce-wrapper/is-redemption-succeeded.ts | 17 ++++++++--------- .../src/commerce-wrapper/redeem-coupons.ts | 10 +++------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts b/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts index 80de78d..5c5e04e 100644 --- a/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts +++ b/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts @@ -2,18 +2,17 @@ import { RedeemCouponsResponse } from '../redeem-coupons' export const isRedemptionSucceeded = ( redemptionResult: RedeemCouponsResponse, - coupon: string -): { isRedemptionSuccessful: boolean } => { - const isRedemptionSuccessful = + coupons: string[] +): boolean => { + const isRedemptionOfAllCouponsSuccessful = redemptionResult && - redemptionResult.redemptions.some( - (redemption) => - redemption.voucher.id === coupon && redemption.result === 'SUCCESS' + redemptionResult.redemptions.every( + (redemption) => redemption.result === 'SUCCESS' ) - if (!isRedemptionSuccessful) { - throw new Error(`Redemption of code: ${coupon} failed.`) + if (!isRedemptionOfAllCouponsSuccessful) { + throw new Error('Redemption failed.') } - return { isRedemptionSuccessful } + return isRedemptionOfAllCouponsSuccessful } diff --git a/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts index 4d86c16..6c55dfe 100644 --- a/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts +++ b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts @@ -23,12 +23,8 @@ export const redeemCouponsFunction = codes: coupons, }) - const redemptionsResults = coupons.map((coupon) => - isRedemptionSucceeded(redemptionResult, coupon) - ) - console.log('redemptionsResults', redemptionsResults) - const response = { - result: true, + const redemptionsResult = isRedemptionSucceeded(redemptionResult, coupons) + return { + result: redemptionsResult, } - return response } From 08b27c6e9f4bbf15a23744e4b59c73a5495e590b Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:27:00 +0100 Subject: [PATCH 21/49] Filter out the promotions from redeemables --- composable-ui/src/hooks/use-checkout.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composable-ui/src/hooks/use-checkout.tsx b/composable-ui/src/hooks/use-checkout.tsx index 31f65c3..36995bb 100644 --- a/composable-ui/src/hooks/use-checkout.tsx +++ b/composable-ui/src/hooks/use-checkout.tsx @@ -53,7 +53,11 @@ export const useCheckout = () => { let redirectUrl const coupons = context.cartSnapshot.redeemables - ?.filter((redeemable) => redeemable.status === 'APPLICABLE') + ?.filter( + (redeemable) => + redeemable.status === 'APPLICABLE' && + redeemable.object === 'voucher' + ) .map((redeemable) => { return redeemable.id }) From 9c59e22daa8d33c7aebc4fb73c6b32691956fd39 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:45:41 +0100 Subject: [PATCH 22/49] Update order-with-discount.ts --- packages/voucherify/data/order-with-discount.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/voucherify/data/order-with-discount.ts b/packages/voucherify/data/order-with-discount.ts index 7fb72db..eadfc57 100644 --- a/packages/voucherify/data/order-with-discount.ts +++ b/packages/voucherify/data/order-with-discount.ts @@ -9,19 +9,19 @@ import { export const orderWithDiscount = ( order: Order | null, cart: Cart, - redemptionResponse: RedemptionsRedeemStackableResponse | false + validationResponse: ValidationValidateStackableResponse | false ): OrderWithDiscounts | null => { const discountAmount = centToString( - redemptionResponse ? redemptionResponse.order?.discount_amount : 0 + validationResponse ? validationResponse.order?.discount_amount : 0 ) const grandPrice = centToString( - redemptionResponse - ? redemptionResponse.order?.total_amount + validationResponse + ? validationResponse.order?.total_amount : toCent(cart.summary.totalPrice) ) const totalDiscountAmount = centToString( - redemptionResponse - ? redemptionResponse.order?.total_applied_discount_amount + validationResponse + ? validationResponse.order?.total_applied_discount_amount : 0 ) if (!order) { From 787299d858d442e1b4c02de943e26dc2d0df276f Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:54:10 +0100 Subject: [PATCH 23/49] Redeem promotions --- composable-ui/src/hooks/use-checkout.tsx | 35 +++++++++---------- .../procedures/checkout/redeem-coupons.ts | 7 +++- .../commerce-service-with-discounts.ts | 5 ++- .../data/get-redeemables-for-validation.ts | 15 +++++--- .../is-redemption-succeeded.ts | 3 +- .../src/commerce-wrapper/redeem-coupons.ts | 20 ++++++++--- packages/voucherify/src/redeem-coupons.ts | 12 ++++--- 7 files changed, 62 insertions(+), 35 deletions(-) diff --git a/composable-ui/src/hooks/use-checkout.tsx b/composable-ui/src/hooks/use-checkout.tsx index 36995bb..7eae7bc 100644 --- a/composable-ui/src/hooks/use-checkout.tsx +++ b/composable-ui/src/hooks/use-checkout.tsx @@ -26,19 +26,21 @@ export const useCheckout = () => { /** * Redeem Coupon Mutation */ - const redeemCouponsMutation = useMutation(async (coupons: string[]) => { - if (!cart.id) { - return - } - const redeemedSuccessfully = await client.commerce.redeemCoupons.mutate({ - cartId: cart.id, - coupons, - }) - if (!redeemedSuccessfully) { - throw new Error('Failed to redeem.') + const redeemCouponsMutation = useMutation( + async (coupons: { id: string; type: string }[]) => { + if (!cart.id) { + return + } + const redeemedSuccessfully = await client.commerce.redeemCoupons.mutate({ + cartId: cart.id, + coupons, + }) + if (!redeemedSuccessfully) { + throw new Error('Failed to redeem.') + } + return redeemedSuccessfully } - return redeemedSuccessfully - }) + ) /** * Place Order Mutation @@ -53,14 +55,11 @@ export const useCheckout = () => { let redirectUrl const coupons = context.cartSnapshot.redeemables - ?.filter( - (redeemable) => - redeemable.status === 'APPLICABLE' && - redeemable.object === 'voucher' - ) + ?.filter((redeemable) => redeemable.status === 'APPLICABLE') .map((redeemable) => { - return redeemable.id + return { id: redeemable.id, type: redeemable.object } }) + if (!__checkoutResponse) { try { if (coupons) { diff --git a/composable-ui/src/server/api/routers/commerce/procedures/checkout/redeem-coupons.ts b/composable-ui/src/server/api/routers/commerce/procedures/checkout/redeem-coupons.ts index 905046e..594ca22 100644 --- a/composable-ui/src/server/api/routers/commerce/procedures/checkout/redeem-coupons.ts +++ b/composable-ui/src/server/api/routers/commerce/procedures/checkout/redeem-coupons.ts @@ -6,7 +6,12 @@ export const redeemCoupons = protectedProcedure .input( z.object({ cartId: z.string(), - coupons: z.array(z.string()), + coupons: z.array( + z.object({ + id: z.string(), + type: z.string(), + }) + ), }) ) .mutation(async ({ input }) => { diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts index fe488b7..bd1f804 100644 --- a/packages/types/src/voucherify/commerce-service-with-discounts.ts +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -40,6 +40,9 @@ export interface CommerceServiceWithDiscounts extends CommerceService { }): Promise redeemCoupons(props: { cartId: string - coupons: string[] + coupons: { + id: string + type: string + }[] }): Promise<{ result: boolean }> } diff --git a/packages/voucherify/data/get-redeemables-for-validation.ts b/packages/voucherify/data/get-redeemables-for-validation.ts index af230c8..772b554 100644 --- a/packages/voucherify/data/get-redeemables-for-validation.ts +++ b/packages/voucherify/data/get-redeemables-for-validation.ts @@ -1,4 +1,7 @@ -import { PromotionsValidateResponse } from '@voucherify/sdk' +import { + PromotionsValidateResponse, + StackableRedeemableObject, +} from '@voucherify/sdk' export const getRedeemablesForValidation = (couponCodes: string[]) => couponCodes.map((couponCode) => ({ @@ -6,10 +9,12 @@ export const getRedeemablesForValidation = (couponCodes: string[]) => object: 'voucher' as const, })) -export const getRedeemablesForRedemption = (couponCodes: string[]) => - couponCodes.map((couponCode) => ({ - id: couponCode, - object: 'voucher' as const, +export const getRedeemablesForRedemption = ( + coupons: { id: string; type: StackableRedeemableObject }[] +) => + coupons.map((coupon) => ({ + id: coupon.id, + object: coupon.type, })) export const getRedeemablesForValidationFromPromotions = ( diff --git a/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts b/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts index 5c5e04e..612d5b5 100644 --- a/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts +++ b/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts @@ -1,8 +1,7 @@ import { RedeemCouponsResponse } from '../redeem-coupons' export const isRedemptionSucceeded = ( - redemptionResult: RedeemCouponsResponse, - coupons: string[] + redemptionResult: RedeemCouponsResponse ): boolean => { const isRedemptionOfAllCouponsSuccessful = redemptionResult && diff --git a/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts index 6c55dfe..86f73b6 100644 --- a/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts +++ b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts @@ -1,5 +1,8 @@ import { CommerceService } from '@composable/types' -import { VoucherifyServerSide } from '@voucherify/sdk' +import { + StackableRedeemableObject, + VoucherifyServerSide, +} from '@voucherify/sdk' import { redeemCoupons } from '../redeem-coupons' import { isRedemptionSucceeded } from './is-redemption-succeeded' @@ -8,7 +11,16 @@ export const redeemCouponsFunction = commerceService: CommerceService, voucherify: ReturnType ) => - async ({ cartId, coupons }: { cartId: string; coupons: string[] }) => { + async ({ + cartId, + coupons, + }: { + cartId: string + coupons: { + id: string + type: StackableRedeemableObject + }[] + }) => { const cart = await commerceService.getCart({ cartId }) if (!cart) { @@ -19,11 +31,11 @@ export const redeemCouponsFunction = const redemptionResult = await redeemCoupons({ cart, + coupons, voucherify, - codes: coupons, }) - const redemptionsResult = isRedemptionSucceeded(redemptionResult, coupons) + const redemptionsResult = isRedemptionSucceeded(redemptionResult) return { result: redemptionsResult, } diff --git a/packages/voucherify/src/redeem-coupons.ts b/packages/voucherify/src/redeem-coupons.ts index d4d6c53..c217aa0 100644 --- a/packages/voucherify/src/redeem-coupons.ts +++ b/packages/voucherify/src/redeem-coupons.ts @@ -6,13 +6,17 @@ import { import { Cart } from '@composable/types' import { RedemptionsRedeemStackableResponse, + StackableRedeemableObject, StackableRedeemableResponse, VoucherifyServerSide, } from '@voucherify/sdk' -type RedeemCouponsParam = { +export type RedeemCouponsParam = { cart: Cart - codes: string[] + coupons: { + id: string + type: StackableRedeemableObject + }[] voucherify: ReturnType } @@ -25,12 +29,12 @@ export type RedeemCouponsResponse = export const redeemCoupons = async ( params: RedeemCouponsParam ): Promise => { - const { cart, codes, voucherify } = params + const { cart, coupons, voucherify } = params const order = cartToVoucherifyOrder(cart) return await voucherify.redemptions.redeemStackable({ - redeemables: [...getRedeemablesForRedemption(codes)], + redeemables: [...getRedeemablesForRedemption(coupons)], order, options: { expand: ['order'] }, }) From a52a8f2dfedcc97f81e9448697d8866f688f4037 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Mon, 13 Nov 2023 19:10:48 +0100 Subject: [PATCH 24/49] If there's no grandPrice, use totalPrice --- composable-ui/src/components/checkout/order-summary.tsx | 7 +++++-- composable-ui/src/hooks/use-checkout.tsx | 8 ++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/composable-ui/src/components/checkout/order-summary.tsx b/composable-ui/src/components/checkout/order-summary.tsx index 951e596..40d245e 100644 --- a/composable-ui/src/components/checkout/order-summary.tsx +++ b/composable-ui/src/components/checkout/order-summary.tsx @@ -17,7 +17,6 @@ import { OrderTotals } from './order-totals' import { ProductsList } from './products-list' import { CartPromotions } from '../cart/cart-promotions' import { CouponForm } from '../forms/coupon-form' -import { CartSummaryProps } from '../cart' export interface CheckoutSidebarProps { itemsBoxProps?: AccordionProps @@ -118,7 +117,11 @@ export const OrderSummary = ({ id: 'checkout.orderSummary.orderTotal', })} total={intl.formatNumber( - parseFloat(_cartData?.summary?.grandPrice ?? '0'), + parseFloat( + _cartData?.summary?.grandPrice || + _cartData?.summary?.totalPrice || + '0' + ), currencyFormatConfig )} totalDiscountAmountTitle={intl.formatMessage({ diff --git a/composable-ui/src/hooks/use-checkout.tsx b/composable-ui/src/hooks/use-checkout.tsx index 7eae7bc..9dc1b78 100644 --- a/composable-ui/src/hooks/use-checkout.tsx +++ b/composable-ui/src/hooks/use-checkout.tsx @@ -63,12 +63,8 @@ export const useCheckout = () => { if (!__checkoutResponse) { try { if (coupons) { - try { - await redeemCouponsMutation.mutateAsync(coupons) - console.log('Redeemed successfully.') - } catch (error) { - throw new Error('Failed to redeem coupon.') - } + await redeemCouponsMutation.mutateAsync(coupons) + console.log('Redeemed successfully.') } const params = { ...state, From b231e84c14706a8a4f912287830a81e01a1d98fb Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:25:31 +0100 Subject: [PATCH 25/49] Add information about applied discount to the mobile view of cart summary --- .../checkout/bag-summary-mobile.tsx | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/composable-ui/src/components/checkout/bag-summary-mobile.tsx b/composable-ui/src/components/checkout/bag-summary-mobile.tsx index 8630b6c..f142f80 100644 --- a/composable-ui/src/components/checkout/bag-summary-mobile.tsx +++ b/composable-ui/src/components/checkout/bag-summary-mobile.tsx @@ -11,21 +11,29 @@ import { import { FormatNumberOptions, useIntl } from 'react-intl' import { Section } from '@composable/ui' import { CartEmptyState, CartLoadingState } from '../cart' -import { useCart, useCheckout } from '../../hooks' +import { CartData, useCart, useCheckout } from '../../hooks' import { APP_CONFIG } from '../../utils/constants' import { OrderTotals } from './order-totals' import { ProductsList } from './products-list' +import { cartData } from '../cart/__data__/cart-data' interface BagSummaryMobileProps { accordionProps?: AccordionProps + cartData?: CartData } -export const BagSummaryMobile = ({ accordionProps }: BagSummaryMobileProps) => { +export const BagSummaryMobile = ({ + accordionProps, + cartData, +}: BagSummaryMobileProps) => { const intl = useIntl() const { cart } = useCart() const { cartSnapshot } = useCheckout() + const _cart = cart.isEmpty ? cartSnapshot : cart + const _cartData = cartData ?? cart + const currencyFormatConfig: FormatNumberOptions = { currency: APP_CONFIG.CURRENCY_CODE, style: 'currency', @@ -38,25 +46,25 @@ export const BagSummaryMobile = ({ accordionProps }: BagSummaryMobileProps) => { {intl.formatMessage( { - id: _cart?.quantity + id: _cartData?.quantity ? 'cart.drawer.titleCount' : 'cart.drawer.title', }, - { count: _cart?.quantity } + { count: _cartData?.quantity } )}{' '} {intl.formatNumber( - parseFloat(_cart?.summary?.totalPrice || '0'), + parseFloat(_cartData?.summary?.grandPrice || '0'), currencyFormatConfig )} - {_cart?.isLoading ? ( + {_cartData?.isLoading ? ( - ) : _cart?.isEmpty ? ( + ) : _cartData?.isEmpty ? ( ) : (
{ }} > - + { id: 'cart.summary.shipping.free', })} tax={intl.formatNumber( - parseFloat(_cart?.summary?.taxes ?? '0'), + parseFloat(_cartData?.summary?.taxes ?? '0'), currencyFormatConfig )} totalTitle={intl.formatMessage({ id: 'checkout.orderSummary.orderTotal', })} total={intl.formatNumber( - parseFloat(_cart?.summary?.totalPrice ?? '0'), + parseFloat(_cartData?.summary?.grandPrice ?? '0'), + currencyFormatConfig + )} + totalDiscountAmountTitle={intl.formatMessage({ + id: 'cart.summary.totalDiscountAmount', + })} + totalDiscountAmount={intl.formatNumber( + parseFloat(_cartData?.summary?.discountAmount ?? '0'), currencyFormatConfig )} /> From 1e527fe7a2be2c71fd1749143a86251788859024 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:19:43 +0100 Subject: [PATCH 26/49] Fix naming --- .../src/services/checkout/create-order.ts | 2 +- .../src/commerce-wrapper/create-order.ts | 30 +------------------ ...cceeded.ts => is-redemption-successful.ts} | 2 +- .../src/commerce-wrapper/redeem-coupons.ts | 8 ++--- 4 files changed, 7 insertions(+), 35 deletions(-) rename packages/voucherify/src/commerce-wrapper/{is-redemption-succeeded.ts => is-redemption-successful.ts} (91%) diff --git a/packages/commerce-generic/src/services/checkout/create-order.ts b/packages/commerce-generic/src/services/checkout/create-order.ts index dbe333e..887f288 100644 --- a/packages/commerce-generic/src/services/checkout/create-order.ts +++ b/packages/commerce-generic/src/services/checkout/create-order.ts @@ -4,7 +4,7 @@ import { saveOrder } from '../../data/persit' import shippingMethods from '../../data/shipping-methods.json' import { randomUUID } from 'crypto' -const generateOrderFromCart = ( +export const generateOrderFromCart = ( cart: Cart, checkoutInput: CheckoutInput ): Order => { diff --git a/packages/voucherify/src/commerce-wrapper/create-order.ts b/packages/voucherify/src/commerce-wrapper/create-order.ts index 71426b9..4d4e8c2 100644 --- a/packages/voucherify/src/commerce-wrapper/create-order.ts +++ b/packages/voucherify/src/commerce-wrapper/create-order.ts @@ -6,6 +6,7 @@ import { getCartDiscounts } from '../../data/persit' import { randomUUID } from 'crypto' import shippingMethods from '../../../commerce-generic/src/data/shipping-methods.json' import { saveOrder } from '../../../commerce-generic/src/data/persit' +import { generateOrderFromCart } from '../../../commerce-generic/src/services' export const createOrderFunction = ( @@ -31,35 +32,6 @@ export const createOrderFunction = codes, }) - const generateOrderFromCart = ( - cart: Cart, - checkoutInput: CheckoutInput - ): Order => { - return { - id: randomUUID(), - status: 'complete', - payment: 'unpaid', - shipping: 'unfulfilled', - customer: { - email: checkoutInput.customer.email, - }, - shipping_address: { - phone_number: '', - city: '', - ...checkoutInput.shipping_address, - }, - billing_address: { - phone_number: '', - city: '', - ...checkoutInput.billing_address, - }, - shipping_method: shippingMethods[0], - created_at: Date.now(), - items: cart.items, - summary: cart.summary, - } - } - const orderWithDiscounts = orderWithDiscount( generateOrderFromCart(cart, props[0].checkout), cart, diff --git a/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts b/packages/voucherify/src/commerce-wrapper/is-redemption-successful.ts similarity index 91% rename from packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts rename to packages/voucherify/src/commerce-wrapper/is-redemption-successful.ts index 612d5b5..339c778 100644 --- a/packages/voucherify/src/commerce-wrapper/is-redemption-succeeded.ts +++ b/packages/voucherify/src/commerce-wrapper/is-redemption-successful.ts @@ -1,6 +1,6 @@ import { RedeemCouponsResponse } from '../redeem-coupons' -export const isRedemptionSucceeded = ( +export const isRedemptionSuccessful = ( redemptionResult: RedeemCouponsResponse ): boolean => { const isRedemptionOfAllCouponsSuccessful = diff --git a/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts index 86f73b6..98078e7 100644 --- a/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts +++ b/packages/voucherify/src/commerce-wrapper/redeem-coupons.ts @@ -4,7 +4,7 @@ import { VoucherifyServerSide, } from '@voucherify/sdk' import { redeemCoupons } from '../redeem-coupons' -import { isRedemptionSucceeded } from './is-redemption-succeeded' +import { isRedemptionSuccessful } from './is-redemption-successful' export const redeemCouponsFunction = ( @@ -25,17 +25,17 @@ export const redeemCouponsFunction = if (!cart) { throw new Error( - `[voucherify][redeemCoupon] cart not found by id: ${cartId}` + `[voucherify][redeemCoupons] cart not found by id: ${cartId}` ) } - const redemptionResult = await redeemCoupons({ + const redemptionsResponse = await redeemCoupons({ cart, coupons, voucherify, }) - const redemptionsResult = isRedemptionSucceeded(redemptionResult) + const redemptionsResult = isRedemptionSuccessful(redemptionsResponse) return { result: redemptionsResult, } From dd6ac49484f2feaf6c574dc66b47be7b825e1654 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:20:57 +0100 Subject: [PATCH 27/49] Update create-order.ts --- packages/voucherify/src/commerce-wrapper/create-order.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/voucherify/src/commerce-wrapper/create-order.ts b/packages/voucherify/src/commerce-wrapper/create-order.ts index 4d4e8c2..c1fbcc1 100644 --- a/packages/voucherify/src/commerce-wrapper/create-order.ts +++ b/packages/voucherify/src/commerce-wrapper/create-order.ts @@ -1,10 +1,8 @@ -import { Cart, CheckoutInput, CommerceService, Order } from '@composable/types' +import { CommerceService } from '@composable/types' import { VoucherifyServerSide } from '@voucherify/sdk' import { orderWithDiscount } from '../../data/order-with-discount' import { validateCouponsAndPromotions } from '../validate-discounts' import { getCartDiscounts } from '../../data/persit' -import { randomUUID } from 'crypto' -import shippingMethods from '../../../commerce-generic/src/data/shipping-methods.json' import { saveOrder } from '../../../commerce-generic/src/data/persit' import { generateOrderFromCart } from '../../../commerce-generic/src/services' From a92d629993751e3513f79bb76c82424e3809e8af Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:29:44 +0100 Subject: [PATCH 28/49] Remove unused imports --- .../src/voucherify/commerce-service-with-discounts.ts | 8 +------- packages/voucherify/data/order-with-discount.ts | 7 ++----- packages/voucherify/src/commerce-wrapper/create-order.ts | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts index bd1f804..c9bac60 100644 --- a/packages/types/src/voucherify/commerce-service-with-discounts.ts +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -1,10 +1,4 @@ -import { - CommerceService, - Cart, - CartItem, - CheckoutInput, - Order, -} from '../commerce' +import { CommerceService, CheckoutInput } from '../commerce' import { CartWithDiscounts } from './cart-with-discounts' import { OrderWithDiscounts } from './order-with-discounts' diff --git a/packages/voucherify/data/order-with-discount.ts b/packages/voucherify/data/order-with-discount.ts index eadfc57..65ba512 100644 --- a/packages/voucherify/data/order-with-discount.ts +++ b/packages/voucherify/data/order-with-discount.ts @@ -1,10 +1,7 @@ import { OrderWithDiscounts } from '@composable/types/src/voucherify/order-with-discounts' import { centToString, toCent } from '../src/to-cent' import { Cart, Order } from '@composable/types' -import { - RedemptionsRedeemStackableResponse, - ValidationValidateStackableResponse, -} from '@voucherify/sdk' +import { ValidationValidateStackableResponse } from '@voucherify/sdk' export const orderWithDiscount = ( order: Order | null, @@ -25,7 +22,7 @@ export const orderWithDiscount = ( : 0 ) if (!order) { - return null + throw new Error('[voucherify][orderWithDiscount] Order not found.') } return { ...order, diff --git a/packages/voucherify/src/commerce-wrapper/create-order.ts b/packages/voucherify/src/commerce-wrapper/create-order.ts index c1fbcc1..c0da3b5 100644 --- a/packages/voucherify/src/commerce-wrapper/create-order.ts +++ b/packages/voucherify/src/commerce-wrapper/create-order.ts @@ -20,7 +20,7 @@ export const createOrderFunction = const codes = await getCartDiscounts(cartId) if (!cart) { - return null + throw new Error('[voucherify][createOrderFunction] No cart found.') } const { validationResult, promotionsResult } = From ae6f8fd713bdf6d03f53f299a3233ff09aaba34c Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:39:23 +0100 Subject: [PATCH 29/49] Refactor --- composable-ui/src/hooks/use-checkout.tsx | 9 ++++----- packages/commerce-generic/src/data/persit.ts | 8 ++------ packages/voucherify/src/commerce-wrapper/create-order.ts | 7 +++++++ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/composable-ui/src/hooks/use-checkout.tsx b/composable-ui/src/hooks/use-checkout.tsx index 9dc1b78..4295b4d 100644 --- a/composable-ui/src/hooks/use-checkout.tsx +++ b/composable-ui/src/hooks/use-checkout.tsx @@ -31,14 +31,14 @@ export const useCheckout = () => { if (!cart.id) { return } - const redeemedSuccessfully = await client.commerce.redeemCoupons.mutate({ + const redemptionSuccessful = await client.commerce.redeemCoupons.mutate({ cartId: cart.id, coupons, }) - if (!redeemedSuccessfully) { - throw new Error('Failed to redeem.') + if (!redemptionSuccessful) { + throw new Error('Failed to redeem coupons.') } - return redeemedSuccessfully + return redemptionSuccessful } ) @@ -64,7 +64,6 @@ export const useCheckout = () => { try { if (coupons) { await redeemCouponsMutation.mutateAsync(coupons) - console.log('Redeemed successfully.') } const params = { ...state, diff --git a/packages/commerce-generic/src/data/persit.ts b/packages/commerce-generic/src/data/persit.ts index c985f03..f61baaf 100644 --- a/packages/commerce-generic/src/data/persit.ts +++ b/packages/commerce-generic/src/data/persit.ts @@ -13,12 +13,8 @@ export const getOrder = async (orderId: string): Promise => { return storage.getItem(`order-${orderId}`) } -export const saveOrder = async (order: Order | null) => { - if (!order) { - return null - } - await storage.setItem(`order-${order.id}`, order) - return order +export const saveOrder = async (order: Order) => { + return storage.setItem(`order-${order.id}`, order) } export const getCart = async (cartId: string): Promise => { diff --git a/packages/voucherify/src/commerce-wrapper/create-order.ts b/packages/voucherify/src/commerce-wrapper/create-order.ts index c0da3b5..4c41e33 100644 --- a/packages/voucherify/src/commerce-wrapper/create-order.ts +++ b/packages/voucherify/src/commerce-wrapper/create-order.ts @@ -35,5 +35,12 @@ export const createOrderFunction = cart, validationResult ) + + if (!orderWithDiscounts) { + throw new Error( + '[voucherify][createOrderFunction] No order with discounts found.' + ) + } + return saveOrder(orderWithDiscounts) } From f22a60ff32c5325996e12eb91e07bc7cf81e203a Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:44:55 +0100 Subject: [PATCH 30/49] Remove required from coupon form --- .../src/components/forms/coupon-form.tsx | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/composable-ui/src/components/forms/coupon-form.tsx b/composable-ui/src/components/forms/coupon-form.tsx index f32bbe3..498e692 100644 --- a/composable-ui/src/components/forms/coupon-form.tsx +++ b/composable-ui/src/components/forms/coupon-form.tsx @@ -1,27 +1,17 @@ import * as yup from 'yup' import { useIntl } from 'react-intl' import { useForm } from 'react-hook-form' -import { - Alert, - AlertIcon, - Box, - Flex, - TagLeftIcon, - Wrap, - WrapItem, -} from '@chakra-ui/react' +import { Alert, AlertIcon, Box, Flex, TagLeftIcon } from '@chakra-ui/react' import { yupResolver } from '@hookform/resolvers/yup' import { useState } from 'react' -import { IconButton, Text } from '@chakra-ui/react' +import { IconButton } from '@chakra-ui/react' import { ArrowForwardIcon } from '@chakra-ui/icons' import { InputField } from '@composable/ui' import { Tag, TagLabel, TagCloseButton } from '@chakra-ui/react' import { useCart } from 'hooks' 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 = () => { const intl = useIntl() @@ -151,6 +141,6 @@ export const CouponForm = () => { const couponFormSchema = () => { return yup.object().shape({ - coupon: yup.string().required(), + coupon: yup.string(), }) } From 26730b6912259359a5b88e6abadb61a93828218d Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:50:20 +0100 Subject: [PATCH 31/49] Update persit.ts --- packages/commerce-generic/src/data/persit.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/commerce-generic/src/data/persit.ts b/packages/commerce-generic/src/data/persit.ts index f61baaf..9dd7eb1 100644 --- a/packages/commerce-generic/src/data/persit.ts +++ b/packages/commerce-generic/src/data/persit.ts @@ -14,7 +14,8 @@ export const getOrder = async (orderId: string): Promise => { } export const saveOrder = async (order: Order) => { - return storage.setItem(`order-${order.id}`, order) + await storage.setItem(`order-${order.id}`, order) + return order } export const getCart = async (cartId: string): Promise => { From 1a598e38cd9deda0a467752afa6e047f2f4cac18 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:11:01 +0100 Subject: [PATCH 32/49] Unused imports --- composable-ui/src/components/checkout/bag-summary-mobile.tsx | 1 - packages/commerce-generic/src/services/checkout/get-order.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/composable-ui/src/components/checkout/bag-summary-mobile.tsx b/composable-ui/src/components/checkout/bag-summary-mobile.tsx index f142f80..96f092c 100644 --- a/composable-ui/src/components/checkout/bag-summary-mobile.tsx +++ b/composable-ui/src/components/checkout/bag-summary-mobile.tsx @@ -15,7 +15,6 @@ import { CartData, useCart, useCheckout } from '../../hooks' import { APP_CONFIG } from '../../utils/constants' import { OrderTotals } from './order-totals' import { ProductsList } from './products-list' -import { cartData } from '../cart/__data__/cart-data' interface BagSummaryMobileProps { accordionProps?: AccordionProps diff --git a/packages/commerce-generic/src/services/checkout/get-order.ts b/packages/commerce-generic/src/services/checkout/get-order.ts index 2a52ac6..b7f5f06 100644 --- a/packages/commerce-generic/src/services/checkout/get-order.ts +++ b/packages/commerce-generic/src/services/checkout/get-order.ts @@ -1,6 +1,5 @@ import { CommerceService } from '@composable/types' import { getOrder as getOrerFromStorage } from '../../data/persit' -import order from '../../data/order.json' import shippingMethods from '../../data/shipping-methods.json' export const getOrder: CommerceService['getOrder'] = async ({ orderId }) => { From ab40080dd957c874071a7888119dd03e3f851b55 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:34:16 +0100 Subject: [PATCH 33/49] Fix localStorage name --- packages/voucherify/data/persit.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/voucherify/data/persit.ts b/packages/voucherify/data/persit.ts index ba928fc..f786e1c 100644 --- a/packages/voucherify/data/persit.ts +++ b/packages/voucherify/data/persit.ts @@ -7,9 +7,9 @@ const storageFolderPath = path.join( 'composable-ui-storage-voucherify' ) -const localStarege = storage.create() +const localStorage = storage.create() -localStarege.init({ +localStorage.init({ dir: storageFolderPath, }) @@ -18,18 +18,18 @@ console.log( ) export const getCartDiscounts = async (cartId: string): Promise => { - return (await localStarege.getItem(`cart-discounts-${cartId}`)) || [] + return (await localStorage.getItem(`cart-discounts-${cartId}`)) || [] } export const saveCartDiscounts = async ( cartId: string, discounts: string[] ) => { - await localStarege.setItem(`cart-discounts-${cartId}`, discounts) + await localStorage.setItem(`cart-discounts-${cartId}`, discounts) return discounts } export const deleteCartDiscounts = async (cartId: string) => { - const result = await localStarege.del(`cart-discounts-${cartId}`) + const result = await localStorage.del(`cart-discounts-${cartId}`) return result.removed } From 81c3aa42e771b5d69ae1a661c631be1bd277df2e Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 15 Nov 2023 14:59:30 +0100 Subject: [PATCH 34/49] Prevent value from being reset when an invalid coupon has been used --- .../voucherify/commerce-service-with-discounts.ts | 11 ++++++----- .../voucherify/src/commerce-wrapper/add-coupon.ts | 15 +++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts index c9bac60..983a0ef 100644 --- a/packages/types/src/voucherify/commerce-service-with-discounts.ts +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -1,4 +1,4 @@ -import { CommerceService, CheckoutInput } from '../commerce' +import { CommerceService, CheckoutInput, Cart } from '../commerce' import { CartWithDiscounts } from './cart-with-discounts' import { OrderWithDiscounts } from './order-with-discounts' @@ -24,10 +24,11 @@ export interface CommerceServiceWithDiscounts extends CommerceService { // Additional commerce endpoints to manage applied coupons - addCoupon(props: { - coupon: string - cartId: string - }): Promise<{ cart: CartWithDiscounts; result: boolean; errorMsg?: string }> + addCoupon(props: { coupon: string; cartId: string }): Promise<{ + cart: CartWithDiscounts | Cart + result: boolean + errorMsg?: string + }> deleteCoupon(props: { coupon: string cartId: string diff --git a/packages/voucherify/src/commerce-wrapper/add-coupon.ts b/packages/voucherify/src/commerce-wrapper/add-coupon.ts index 7472937..e25c3d3 100644 --- a/packages/voucherify/src/commerce-wrapper/add-coupon.ts +++ b/packages/voucherify/src/commerce-wrapper/add-coupon.ts @@ -1,9 +1,10 @@ -import { CommerceService } from '@composable/types' +import { commerce } from '../../../../composable-ui/src/server/data-source' import { cartWithDiscount } from '../../data/cart-with-discount' import { VoucherifyServerSide } from '@voucherify/sdk' import { getCartDiscounts, saveCartDiscounts } from '../../data/persit' import { validateCouponsAndPromotions } from '../validate-discounts' import { isRedeemableApplicable } from './is-redeemable-applicable' +import { CommerceService } from '@composable/types' export const addCouponFunction = ( @@ -11,7 +12,9 @@ export const addCouponFunction = voucherify: ReturnType ) => async ({ cartId, coupon }: { cartId: string; coupon: string }) => { - const cart = await commerceService.getCart({ cartId }) + const cart = + (await commerce.getCart({ cartId })) || + (await commerceService.getCart({ cartId })) if (!cart) { throw new Error(`[voucherify][addCoupon] cart not found by id: ${cartId}`) @@ -33,10 +36,14 @@ export const addCouponFunction = if (isApplicable) { await saveCartDiscounts(cartId, [...cartDiscounts, coupon]) + return { + cart: cartWithDiscount(cart, validationResult, promotionsResult), + result: isApplicable, + errorMsg: error, + } } - return { - cart: cartWithDiscount(cart, validationResult, promotionsResult), + cart: cart, result: isApplicable, errorMsg: error, } From 23f4107a4bfcc402ee598d9c562ba15598082739 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:37:49 +0100 Subject: [PATCH 35/49] Unused imports in cart promotions --- .../src/components/cart/cart-promotions.tsx | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/composable-ui/src/components/cart/cart-promotions.tsx b/composable-ui/src/components/cart/cart-promotions.tsx index c559c1a..8d95e0a 100644 --- a/composable-ui/src/components/cart/cart-promotions.tsx +++ b/composable-ui/src/components/cart/cart-promotions.tsx @@ -1,26 +1,9 @@ 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 { Redeemable } from '@composable/types' +import { Box, Flex, Tag, TagLabel, TagLeftIcon } from '@chakra-ui/react' import { MdShoppingCart } from 'react-icons/md' import { Price } from 'components/price' -import { QuantityPicker } from 'components/quantity-picker' -import { CartItemData, CartSummaryItem } from '.' +import { CartSummaryItem } from '.' interface CartPromotionsProps { promotions: Redeemable[] From b6c896a7581fdace53a990c783492c43bc9958f5 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:45:39 +0100 Subject: [PATCH 36/49] Add Dividers to cart summary --- composable-ui/src/components/cart/cart-summary.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composable-ui/src/components/cart/cart-summary.tsx b/composable-ui/src/components/cart/cart-summary.tsx index 2d971f2..d1cca0b 100644 --- a/composable-ui/src/components/cart/cart-summary.tsx +++ b/composable-ui/src/components/cart/cart-summary.tsx @@ -101,8 +101,11 @@ export const CartSummary = ({ )} + + + {_cartData.summary?.totalDiscountAmount && ( Date: Tue, 21 Nov 2023 14:12:50 +0100 Subject: [PATCH 37/49] Add grey stacks for promotions and pass green colour for discounts --- composable-ui/src/components/checkout/order-summary.tsx | 9 +++++++-- composable-ui/src/components/checkout/order-totals.tsx | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/composable-ui/src/components/checkout/order-summary.tsx b/composable-ui/src/components/checkout/order-summary.tsx index 40d245e..e86682d 100644 --- a/composable-ui/src/components/checkout/order-summary.tsx +++ b/composable-ui/src/components/checkout/order-summary.tsx @@ -98,8 +98,13 @@ export const OrderSummary = ({ - - + + + + + + + {totalDiscountAmount && ( )} @@ -82,7 +84,10 @@ const CartSummaryItem = (props: CartSummaryItemProps) => { return ( {label} - + {isDiscount && '-'} {value} From e9faea615a2f35d9e1beee4e584e8eb76fa972ff Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:41:04 +0100 Subject: [PATCH 38/49] Wrapper for addOrder - add discount info to final step --- .../components/checkout/checkout-success-page.tsx | 11 ++++++++++- .../checkout/success/success-order-summary.tsx | 6 ++++++ .../src/services/checkout/get-order.ts | 4 ++-- .../voucherify/commerce-service-with-discounts.ts | 3 +++ .../voucherify/src/commerce-wrapper/get-order.ts | 14 ++++++++++++++ packages/voucherify/src/commerce-wrapper/index.ts | 2 ++ 6 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 packages/voucherify/src/commerce-wrapper/get-order.ts diff --git a/composable-ui/src/components/checkout/checkout-success-page.tsx b/composable-ui/src/components/checkout/checkout-success-page.tsx index ba7d5ad..f6269f8 100644 --- a/composable-ui/src/components/checkout/checkout-success-page.tsx +++ b/composable-ui/src/components/checkout/checkout-success-page.tsx @@ -150,7 +150,16 @@ export const CheckoutSuccessPage = ({ style: 'currency', })} total={intl.formatNumber( - parseFloat(order?.summary.totalPrice ?? '0'), + parseFloat( + order?.summary.grandPrice ?? order?.summary.totalPrice ?? '0' + ), + { + currency: APP_CONFIG.CURRENCY_CODE, + style: 'currency', + } + )} + totalDiscountAmount={intl.formatNumber( + parseFloat(order?.summary.totalDiscountAmount || '0'), { currency: APP_CONFIG.CURRENCY_CODE, style: 'currency', diff --git a/composable-ui/src/components/checkout/success/success-order-summary.tsx b/composable-ui/src/components/checkout/success/success-order-summary.tsx index fb02712..1fc474d 100644 --- a/composable-ui/src/components/checkout/success/success-order-summary.tsx +++ b/composable-ui/src/components/checkout/success/success-order-summary.tsx @@ -13,6 +13,7 @@ interface OrderSummaryProps { tax: string discount?: string total: string + totalDiscountAmount?: string } export const SuccessOrderSummary = ({ @@ -24,6 +25,7 @@ export const SuccessOrderSummary = ({ tax, discount, total, + totalDiscountAmount, }: OrderSummaryProps) => { const intl = useIntl() @@ -60,6 +62,10 @@ export const SuccessOrderSummary = ({ id: 'checkout.success.orderSummary.totalPaid', })} total={total} + totalDiscountAmountTitle={intl.formatMessage({ + id: 'cart.summary.totalDiscountAmount', + })} + totalDiscountAmount={totalDiscountAmount} /> ) diff --git a/packages/commerce-generic/src/services/checkout/get-order.ts b/packages/commerce-generic/src/services/checkout/get-order.ts index b7f5f06..7f79329 100644 --- a/packages/commerce-generic/src/services/checkout/get-order.ts +++ b/packages/commerce-generic/src/services/checkout/get-order.ts @@ -1,9 +1,9 @@ import { CommerceService } from '@composable/types' -import { getOrder as getOrerFromStorage } from '../../data/persit' +import { getOrder as getOrderFromStorage } from '../../data/persit' import shippingMethods from '../../data/shipping-methods.json' export const getOrder: CommerceService['getOrder'] = async ({ orderId }) => { - const order = await getOrerFromStorage(orderId) + const order = await getOrderFromStorage(orderId) if (!order) { throw new Error(`[getOrder] Could not found order: ${orderId}`) diff --git a/packages/types/src/voucherify/commerce-service-with-discounts.ts b/packages/types/src/voucherify/commerce-service-with-discounts.ts index 983a0ef..0a85229 100644 --- a/packages/types/src/voucherify/commerce-service-with-discounts.ts +++ b/packages/types/src/voucherify/commerce-service-with-discounts.ts @@ -21,6 +21,9 @@ export interface CommerceServiceWithDiscounts extends CommerceService { createOrder(params: { checkout: CheckoutInput }): Promise + getOrder( + ...params: Parameters + ): Promise // Additional commerce endpoints to manage applied coupons diff --git a/packages/voucherify/src/commerce-wrapper/get-order.ts b/packages/voucherify/src/commerce-wrapper/get-order.ts new file mode 100644 index 0000000..ad591d6 --- /dev/null +++ b/packages/voucherify/src/commerce-wrapper/get-order.ts @@ -0,0 +1,14 @@ +import { CommerceService } from '@composable/types' +import { VoucherifyServerSide } from '@voucherify/sdk' +import { OrderWithDiscounts } from '@composable/types/src/voucherify/order-with-discounts' + +export const getOrderFunction = + ( + commerceService: CommerceService, + voucherify: ReturnType + ) => + async ( + ...props: Parameters + ): Promise => { + return await commerceService.getOrder(...props) + } diff --git a/packages/voucherify/src/commerce-wrapper/index.ts b/packages/voucherify/src/commerce-wrapper/index.ts index 87a446a..71ebd56 100644 --- a/packages/voucherify/src/commerce-wrapper/index.ts +++ b/packages/voucherify/src/commerce-wrapper/index.ts @@ -12,6 +12,7 @@ import { addCouponFunction } from './add-coupon' import { deleteCouponFunction } from './delete-coupon' import { createOrderFunction } from './create-order' import { redeemCouponsFunction } from './redeem-coupons' +import { getOrderFunction } from './get-order' if ( !process.env.VOUCHERIFY_APPLICATION_ID || @@ -44,5 +45,6 @@ export const commerceWithDiscount = ( deleteCoupon: deleteCouponFunction(commerceService, voucherify), redeemCoupons: redeemCouponsFunction(commerceService, voucherify), createOrder: createOrderFunction(commerceService, voucherify), + getOrder: getOrderFunction(commerceService, voucherify), } } From fcfa832c06a6a272bf8c18784a131952b42ab9dc Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:45:39 +0100 Subject: [PATCH 39/49] Display promotion stack condionally --- composable-ui/src/components/checkout/order-summary.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/composable-ui/src/components/checkout/order-summary.tsx b/composable-ui/src/components/checkout/order-summary.tsx index e86682d..5dac6fc 100644 --- a/composable-ui/src/components/checkout/order-summary.tsx +++ b/composable-ui/src/components/checkout/order-summary.tsx @@ -98,9 +98,11 @@ export const OrderSummary = ({ - - - + {promotions.length > 0 && ( + + + + )} From 5c98f57b9cbcb165e19ae49ad6c1ba637cb9e05b Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 21 Nov 2023 17:42:49 +0100 Subject: [PATCH 40/49] Better cart drawer --- .../cart/cart-drawer/cart-drawer-footer.tsx | 2 +- .../cart/cart-drawer/cart-drawer-summary.tsx | 50 +++++++++---------- .../cart/cart-drawer/cart-drawer.tsx | 10 +++- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx index bc0208f..48ae2b4 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx @@ -29,7 +29,7 @@ export const CartDrawerFooter = ({ cartData }: CartSummaryProps) => { color={'text-muted'} textStyle={{ base: 'Mobile/Eyebrow', md: 'Desktop/Body-XS' }} > - {intl.formatMessage({ id: 'cart.summary.estimatedTotal' })} + {intl.formatMessage({ id: 'cart.summary.grandPrice' })} diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx index 2a0d2ce..d0dac8f 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx @@ -50,40 +50,36 @@ export const CartDrawerSummary = ({ cartData }: CartSummaryProps) => { {_cartData.summary?.totalPrice && ( - <> - - - {intl.formatMessage({ id: 'cart.summary.estimatedTotal' })} - - - - - - + + + + + )} {_cartData.summary?.totalDiscountAmount && ( - - - + + + + )} - + {_cartData.summary?.grandPrice && ( <> - { ) })} - - + {promotions.length > 0 && ( + + + + )} + + + )} From 66a74927320a1c4b57a6fcf38cc0f54914975b69 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Tue, 21 Nov 2023 18:02:46 +0100 Subject: [PATCH 41/49] Fix naming in checkout --- .../checkout/checkout-success-page.tsx | 11 +++++--- .../src/components/checkout/order-summary.tsx | 13 ++++++---- .../src/components/checkout/order-totals.tsx | 25 +++++++++++++------ .../success/success-order-summary.tsx | 8 +++++- composable-ui/src/server/intl/en-US.json | 2 ++ 5 files changed, 42 insertions(+), 17 deletions(-) diff --git a/composable-ui/src/components/checkout/checkout-success-page.tsx b/composable-ui/src/components/checkout/checkout-success-page.tsx index f6269f8..ac7fae5 100644 --- a/composable-ui/src/components/checkout/checkout-success-page.tsx +++ b/composable-ui/src/components/checkout/checkout-success-page.tsx @@ -150,9 +150,7 @@ export const CheckoutSuccessPage = ({ style: 'currency', })} total={intl.formatNumber( - parseFloat( - order?.summary.grandPrice ?? order?.summary.totalPrice ?? '0' - ), + parseFloat(order?.summary.totalPrice ?? '0'), { currency: APP_CONFIG.CURRENCY_CODE, style: 'currency', @@ -165,6 +163,13 @@ export const CheckoutSuccessPage = ({ style: 'currency', } )} + grandPrice={intl.formatNumber( + parseFloat(order?.summary.grandPrice || '0'), + { + currency: APP_CONFIG.CURRENCY_CODE, + style: 'currency', + } + )} /> diff --git a/composable-ui/src/components/checkout/order-summary.tsx b/composable-ui/src/components/checkout/order-summary.tsx index 5dac6fc..103bca9 100644 --- a/composable-ui/src/components/checkout/order-summary.tsx +++ b/composable-ui/src/components/checkout/order-summary.tsx @@ -124,11 +124,7 @@ export const OrderSummary = ({ id: 'checkout.orderSummary.orderTotal', })} total={intl.formatNumber( - parseFloat( - _cartData?.summary?.grandPrice || - _cartData?.summary?.totalPrice || - '0' - ), + parseFloat(_cartData?.summary?.totalPrice || '0'), currencyFormatConfig )} totalDiscountAmountTitle={intl.formatMessage({ @@ -138,6 +134,13 @@ export const OrderSummary = ({ parseFloat(_cartData?.summary?.totalDiscountAmount || '0'), currencyFormatConfig )} + grandPriceTitle={intl.formatMessage({ + id: 'cart.summary.grandPrice', + })} + grandPrice={intl.formatNumber( + parseFloat(_cartData?.summary?.grandPrice || '0'), + currencyFormatConfig + )} /> diff --git a/composable-ui/src/components/checkout/order-totals.tsx b/composable-ui/src/components/checkout/order-totals.tsx index 2c8e533..f9f5543 100644 --- a/composable-ui/src/components/checkout/order-totals.tsx +++ b/composable-ui/src/components/checkout/order-totals.tsx @@ -12,6 +12,8 @@ interface OrderTotalsProps { total: string totalDiscountAmountTitle?: string totalDiscountAmount?: string + grandPriceTitle?: string + grandPrice?: string } export const OrderTotals = ({ @@ -23,6 +25,8 @@ export const OrderTotals = ({ total, totalDiscountAmountTitle, totalDiscountAmount, + grandPriceTitle, + grandPrice, }: OrderTotalsProps) => { const intl = useIntl() @@ -51,6 +55,8 @@ export const OrderTotals = ({ label={intl.formatMessage({ id: 'cart.summary.tax' })} value={tax} /> + + {totalDiscountAmount && ( )} + + {grandPrice && ( + + )} - ) } diff --git a/composable-ui/src/components/checkout/success/success-order-summary.tsx b/composable-ui/src/components/checkout/success/success-order-summary.tsx index 1fc474d..c33a37c 100644 --- a/composable-ui/src/components/checkout/success/success-order-summary.tsx +++ b/composable-ui/src/components/checkout/success/success-order-summary.tsx @@ -14,6 +14,7 @@ interface OrderSummaryProps { discount?: string total: string totalDiscountAmount?: string + grandPrice?: string } export const SuccessOrderSummary = ({ @@ -26,6 +27,7 @@ export const SuccessOrderSummary = ({ discount, total, totalDiscountAmount, + grandPrice, }: OrderSummaryProps) => { const intl = useIntl() @@ -59,13 +61,17 @@ export const SuccessOrderSummary = ({ tax={tax} discount={discount} totalTitle={intl.formatMessage({ - id: 'checkout.success.orderSummary.totalPaid', + id: 'cart.summary.orderTotal', })} total={total} totalDiscountAmountTitle={intl.formatMessage({ id: 'cart.summary.totalDiscountAmount', })} totalDiscountAmount={totalDiscountAmount} + grandPriceTitle={intl.formatMessage({ + id: 'checkout.success.orderSummary.totalPaid', + })} + grandPrice={grandPrice} /> ) diff --git a/composable-ui/src/server/intl/en-US.json b/composable-ui/src/server/intl/en-US.json index 21e6c5e..c73fa87 100644 --- a/composable-ui/src/server/intl/en-US.json +++ b/composable-ui/src/server/intl/en-US.json @@ -191,6 +191,8 @@ "checkout.success.orderDetails.shippingAddress": "Shipping Address", "checkout.success.orderSummary.title": "Order Summary", "checkout.success.orderSummary.totalPaid": "Total Paid", + "checkout.success.orderSummary.orderTotal": "Order Total", + "checkout.success.orderSummary.totalDiscountAmount": "All discounts", "product.returnPolicy.title": "Return Policy", "product.returnsAndShippingPolicy.title": "Return & Shipping", From 709d06cd8d88cce2de86d8638bec8c721c75aa4b Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:10:40 +0100 Subject: [PATCH 42/49] Remove additional button "proceed to checkout" --- .../src/components/cart/cart-total.tsx | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/composable-ui/src/components/cart/cart-total.tsx b/composable-ui/src/components/cart/cart-total.tsx index d95d9ab..8d04269 100644 --- a/composable-ui/src/components/cart/cart-total.tsx +++ b/composable-ui/src/components/cart/cart-total.tsx @@ -1,19 +1,17 @@ import { useIntl } from 'react-intl' import { useRouter } from 'next/router' -import { CartData, useCart } from 'hooks' +import { useCart } from 'hooks' import { Price } from 'components/price' -import { Button, Flex, Text, FlexProps } from '@chakra-ui/react' +import { Flex, Text, FlexProps } from '@chakra-ui/react' interface CartTotalProps { rootProps?: FlexProps - cartData?: CartData } -export const CartTotal = ({ cartData, rootProps }: CartTotalProps) => { +export const CartTotal = ({ rootProps }: CartTotalProps) => { const router = useRouter() const { cart } = useCart() const intl = useIntl() - const _cartData = cartData ?? cart return ( <> @@ -23,20 +21,9 @@ export const CartTotal = ({ cartData, rootProps }: CartTotalProps) => { mb={'1rem'} {...rootProps} > - {intl.formatMessage({ id: 'cart.summary.estimatedTotal' })} - + {intl.formatMessage({ id: 'cart.summary.orderTotal' })} + - ) } From 40e2e0c03a94409b473f3a760f5a00d51e5c666b Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:13:13 +0100 Subject: [PATCH 43/49] Use cart instead of _cartData --- .../cart/cart-drawer/cart-drawer-footer.tsx | 6 +-- .../cart/cart-drawer/cart-drawer-summary.tsx | 29 ++++++-------- .../cart/cart-drawer/cart-drawer.tsx | 8 ++-- .../src/components/cart/cart-summary.tsx | 36 ++++++++--------- .../checkout/bag-summary-mobile.tsx | 40 +++++++++---------- .../src/components/checkout/order-summary.tsx | 20 ++++------ .../src/components/checkout/order-totals.tsx | 8 +--- 7 files changed, 62 insertions(+), 85 deletions(-) diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx index 48ae2b4..a465755 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer-footer.tsx @@ -11,14 +11,12 @@ import { Text, VStack, } from '@chakra-ui/react' -import { CartSummaryProps } from '../cart-summary' -export const CartDrawerFooter = ({ cartData }: CartSummaryProps) => { +export const CartDrawerFooter = () => { const router = useRouter() const { cartDrawer } = useComposable() const { cart } = useCart() const intl = useIntl() - const _cartData = cartData ?? cart return ( @@ -35,7 +33,7 @@ export const CartDrawerFooter = ({ cartData }: CartSummaryProps) => { diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx index d0dac8f..a90dcbb 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer-summary.tsx @@ -3,66 +3,63 @@ import { useIntl } from 'react-intl' import { useCart } from 'hooks' import { CartDrawerSummaryItem } from './cart-drawer-summary-item' import { Price } from '../../price' -import { CartSummaryItem } from '../cart-summary-item' -import { CartSummaryProps } from '../cart-summary' -export const CartDrawerSummary = ({ cartData }: CartSummaryProps) => { +export const CartDrawerSummary = () => { const { cart } = useCart() const intl = useIntl() - const _cartData = cartData ?? cart return ( - {_cartData.summary?.subtotalPrice && ( + {cart.summary?.subtotalPrice && ( )} - {_cartData.summary?.taxes && ( + {cart.summary?.taxes && ( )} - {_cartData.summary?.shipping && ( + {cart.summary?.shipping && ( )} - {_cartData.summary?.totalPrice && ( + {cart.summary?.totalPrice && ( )} - {_cartData.summary?.totalDiscountAmount && ( + {cart.summary?.totalDiscountAmount && ( @@ -72,13 +69,13 @@ export const CartDrawerSummary = ({ cartData }: CartSummaryProps) => { textStyle: 'Body-XS', color: 'green', }} - price={`-${_cartData.summary.totalDiscountAmount}`} + price={`-${cart.summary.totalDiscountAmount}`} /> )} - {_cartData.summary?.grandPrice && ( + {cart.summary?.grandPrice && ( <> { > {intl.formatMessage({ id: 'cart.summary.grandPrice' })} - + diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx index a5e4e0d..2f8a8df 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx @@ -17,7 +17,7 @@ import { Text, } from '@chakra-ui/react' import { APP_CONFIG } from '../../../utils/constants' -import { CartLoadingState, CartSummaryProps } from '../.' +import { CartLoadingState } from '../.' import { CartDrawerFooter } from './cart-drawer-footer' import { CartDrawerSummary } from './cart-drawer-summary' import { CartDrawerEmptyState } from './cart-drawer-empty-state' @@ -25,7 +25,7 @@ import { HorizontalProductCard } from '@composable/ui' import { CouponForm } from '../../forms/coupon-form' import { CartPromotions } from '../cart-promotions' -export const CartDrawer = ({ cartData }: CartSummaryProps) => { +export const CartDrawer = () => { const intl = useIntl() const toast = useToast() const router = useRouter() @@ -40,8 +40,6 @@ export const CartDrawer = ({ cartData }: CartSummaryProps) => { }, }) - const _cartData = cartData ?? cart - const title = intl.formatMessage( { id: 'cart.drawer.titleCount' }, { count: cart.quantity } @@ -52,7 +50,7 @@ export const CartDrawer = ({ cartData }: CartSummaryProps) => { } const promotions = - _cartData.redeemables?.filter( + cart.redeemables?.filter( (redeemable) => redeemable.object === 'promotion_tier' ) || [] diff --git a/composable-ui/src/components/cart/cart-summary.tsx b/composable-ui/src/components/cart/cart-summary.tsx index d1cca0b..4f3ffa5 100644 --- a/composable-ui/src/components/cart/cart-summary.tsx +++ b/composable-ui/src/components/cart/cart-summary.tsx @@ -1,6 +1,6 @@ import { useIntl } from 'react-intl' import { useRouter } from 'next/router' -import { CartData, useCart } from 'hooks' +import { useCart } from 'hooks' import { Price } from 'components/price' import { CouponForm } from 'components/forms/coupon-form' import { @@ -18,25 +18,21 @@ import { CartPromotions } from './cart-promotions' export interface CartSummaryProps { rootProps?: StackProps renderCheckoutButton?: boolean - cartData?: CartData } export const CartSummary = ({ rootProps, renderCheckoutButton = true, - cartData, }: CartSummaryProps) => { const router = useRouter() const { cart } = useCart() const intl = useIntl() - const _cartData = cartData ?? cart const vouchers = - _cartData.redeemables?.filter( - (redeemable) => redeemable.object === 'voucher' - ) || [] + cart.redeemables?.filter((redeemable) => redeemable.object === 'voucher') || + [] const promotions = - _cartData.redeemables?.filter( + cart.redeemables?.filter( (redeemable) => redeemable.object === 'promotion_tier' ) || [] @@ -53,39 +49,39 @@ export const CartSummary = ({ - {_cartData.summary?.subtotalPrice && ( + {cart.summary?.subtotalPrice && ( )} - {_cartData.summary?.shipping && ( + {cart.summary?.shipping && ( )} - {_cartData.summary?.taxes && ( + {cart.summary?.taxes && ( )} - {_cartData.summary?.totalPrice && ( + {cart.summary?.totalPrice && ( <> - + @@ -106,7 +102,7 @@ export const CartSummary = ({ - {_cartData.summary?.totalDiscountAmount && ( + {cart.summary?.totalDiscountAmount && ( )} - {_cartData.summary?.grandPrice && ( + {cart.summary?.grandPrice && ( <> - + diff --git a/composable-ui/src/components/checkout/bag-summary-mobile.tsx b/composable-ui/src/components/checkout/bag-summary-mobile.tsx index 96f092c..03ef481 100644 --- a/composable-ui/src/components/checkout/bag-summary-mobile.tsx +++ b/composable-ui/src/components/checkout/bag-summary-mobile.tsx @@ -11,27 +11,18 @@ import { import { FormatNumberOptions, useIntl } from 'react-intl' import { Section } from '@composable/ui' import { CartEmptyState, CartLoadingState } from '../cart' -import { CartData, useCart, useCheckout } from '../../hooks' +import { useCart } from '../../hooks' import { APP_CONFIG } from '../../utils/constants' import { OrderTotals } from './order-totals' import { ProductsList } from './products-list' interface BagSummaryMobileProps { accordionProps?: AccordionProps - cartData?: CartData } -export const BagSummaryMobile = ({ - accordionProps, - cartData, -}: BagSummaryMobileProps) => { +export const BagSummaryMobile = ({ accordionProps }: BagSummaryMobileProps) => { const intl = useIntl() const { cart } = useCart() - const { cartSnapshot } = useCheckout() - - const _cart = cart.isEmpty ? cartSnapshot : cart - - const _cartData = cartData ?? cart const currencyFormatConfig: FormatNumberOptions = { currency: APP_CONFIG.CURRENCY_CODE, @@ -45,25 +36,25 @@ export const BagSummaryMobile = ({ {intl.formatMessage( { - id: _cartData?.quantity + id: cart?.quantity ? 'cart.drawer.titleCount' : 'cart.drawer.title', }, - { count: _cartData?.quantity } + { count: cart?.quantity } )}{' '} {intl.formatNumber( - parseFloat(_cartData?.summary?.grandPrice || '0'), + parseFloat(cart?.summary?.grandPrice || '0'), currencyFormatConfig )} - {_cartData?.isLoading ? ( + {cart?.isLoading ? ( - ) : _cartData?.isEmpty ? ( + ) : cart?.isEmpty ? ( ) : (
- + diff --git a/composable-ui/src/components/checkout/order-summary.tsx b/composable-ui/src/components/checkout/order-summary.tsx index 103bca9..70b1690 100644 --- a/composable-ui/src/components/checkout/order-summary.tsx +++ b/composable-ui/src/components/checkout/order-summary.tsx @@ -10,7 +10,7 @@ import { Stack, Text, } from '@chakra-ui/react' -import { CartData, useCart, useCheckout } from 'hooks' +import { useCart, useCheckout } from 'hooks' import { FormatNumberOptions, useIntl } from 'react-intl' import { APP_CONFIG } from '../../utils/constants' import { OrderTotals } from './order-totals' @@ -21,13 +21,11 @@ import { CouponForm } from '../forms/coupon-form' export interface CheckoutSidebarProps { itemsBoxProps?: AccordionProps showTitle?: boolean - cartData?: CartData } export const OrderSummary = ({ itemsBoxProps, showTitle = true, - cartData, }: CheckoutSidebarProps) => { const intl = useIntl() const { cart } = useCart() @@ -39,10 +37,8 @@ export const OrderSummary = ({ style: 'currency', } - const _cartData = cartData ?? cart - const promotions = - _cartData.redeemables?.filter( + cart.redeemables?.filter( (redeemable) => redeemable.object === 'promotion_tier' ) || [] @@ -106,10 +102,10 @@ export const OrderSummary = ({ - + diff --git a/composable-ui/src/components/checkout/order-totals.tsx b/composable-ui/src/components/checkout/order-totals.tsx index f9f5543..08b213b 100644 --- a/composable-ui/src/components/checkout/order-totals.tsx +++ b/composable-ui/src/components/checkout/order-totals.tsx @@ -1,6 +1,5 @@ import { Divider, Flex, Stack, Text, TextProps } from '@chakra-ui/react' import { useIntl } from 'react-intl' -import { Price } from '../price' interface OrderTotalsProps { subtotal: string @@ -31,12 +30,7 @@ export const OrderTotals = ({ const intl = useIntl() return ( - } - px={{ base: 4, md: 'none' }} - > + } px={{ base: 4, md: 'none' }}> Date: Wed, 22 Nov 2023 09:19:21 +0100 Subject: [PATCH 44/49] Styling of cart drawer --- composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx | 4 ++-- composable-ui/src/components/cart/cart-promotions.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx index 2f8a8df..9318c14 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx @@ -156,11 +156,11 @@ export const CartDrawer = () => { })} {promotions.length > 0 && ( - + )} - + diff --git a/composable-ui/src/components/cart/cart-promotions.tsx b/composable-ui/src/components/cart/cart-promotions.tsx index 8d95e0a..d97b17a 100644 --- a/composable-ui/src/components/cart/cart-promotions.tsx +++ b/composable-ui/src/components/cart/cart-promotions.tsx @@ -11,7 +11,6 @@ interface CartPromotionsProps { export const CartPromotions = ({ promotions }: CartPromotionsProps) => { const intl = useIntl() - // const isMobile = useBreakpointValue({ base: true, md: false }) if (!promotions.length) { return null } From 738ac78a1e7d47639ce940e592ed3076e0058560 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:40:54 +0100 Subject: [PATCH 45/49] Update warning appearance --- composable-ui/src/components/forms/coupon-form.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/composable-ui/src/components/forms/coupon-form.tsx b/composable-ui/src/components/forms/coupon-form.tsx index 498e692..82db7b7 100644 --- a/composable-ui/src/components/forms/coupon-form.tsx +++ b/composable-ui/src/components/forms/coupon-form.tsx @@ -19,7 +19,6 @@ export const CouponForm = () => { const { register, handleSubmit, - setError, setValue, formState: { errors }, } = useForm<{ coupon: string }>({ @@ -73,7 +72,7 @@ export const CouponForm = () => { flexDirection={'row'} alignItems={'flex-start'} justifyContent={'center'} - height={'60px'} + height={'3em'} gap={3} > { /> {errorMessage && ( - + {errorMessage} From 941ba18705f9eafaf0ee34e404d33dccc4bf4802 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 22 Nov 2023 09:56:44 +0100 Subject: [PATCH 46/49] Change the font size in cart --- composable-ui/src/components/cart/cart-summary.tsx | 2 +- composable-ui/src/components/cart/cart-total.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/composable-ui/src/components/cart/cart-summary.tsx b/composable-ui/src/components/cart/cart-summary.tsx index 4f3ffa5..d8a3063 100644 --- a/composable-ui/src/components/cart/cart-summary.tsx +++ b/composable-ui/src/components/cart/cart-summary.tsx @@ -41,7 +41,7 @@ export const CartSummary = ({ diff --git a/composable-ui/src/components/cart/cart-total.tsx b/composable-ui/src/components/cart/cart-total.tsx index 8d04269..093cea8 100644 --- a/composable-ui/src/components/cart/cart-total.tsx +++ b/composable-ui/src/components/cart/cart-total.tsx @@ -1,5 +1,4 @@ import { useIntl } from 'react-intl' -import { useRouter } from 'next/router' import { useCart } from 'hooks' import { Price } from 'components/price' import { Flex, Text, FlexProps } from '@chakra-ui/react' @@ -9,7 +8,6 @@ interface CartTotalProps { } export const CartTotal = ({ rootProps }: CartTotalProps) => { - const router = useRouter() const { cart } = useCart() const intl = useIntl() From 7aab5ccf0db6dceeecfae30a4ff4111d777db5c9 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:32:57 +0100 Subject: [PATCH 47/49] Fix - type of data returned onCartCouponAddSuccess --- composable-ui/src/hooks/use-cart.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/composable-ui/src/hooks/use-cart.ts b/composable-ui/src/hooks/use-cart.ts index 825f773..9850873 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 { CartWithDiscounts } from '@composable/types' +import { CartWithDiscounts, Cart } from '@composable/types' import { useSession } from 'next-auth/react' const USE_CART_KEY = 'useCartKey' @@ -39,7 +39,17 @@ interface UseCartOptions { onCartCouponDeleteError?: () => void onCartItemUpdateError?: () => void onCartItemDeleteError?: () => void - onCartCouponAddSuccess?: (response: { cart: CartWithDiscounts }) => void + onCartCouponAddSuccess?: ( + data: { + cart: CartWithDiscounts | Cart + result: boolean + }, + variables: { + cartId: string + coupon: string + }, + context: unknown + ) => void onCartCouponDeleteSuccess?: (cart: CartWithDiscounts) => void onCartItemAddSuccess?: (cart: CartWithDiscounts) => void } @@ -270,7 +280,7 @@ export const useCart = (options?: UseCartOptions) => { const cartCouponAddMutation = useCallback( async (params: { cartId: string; coupon: string }) => { const id = cartId ? cartId : await cartCreate.mutateAsync() - await cartCouponAdd.mutate( + cartCouponAdd.mutate( { cartId: id, coupon: params.coupon, From 02713cd54b5a8ad44cfa005032721db367973008 Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:06:38 +0100 Subject: [PATCH 48/49] Restore previous CartSummary --- .../src/components/cart/cart-summary.tsx | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/composable-ui/src/components/cart/cart-summary.tsx b/composable-ui/src/components/cart/cart-summary.tsx index d8a3063..20d381e 100644 --- a/composable-ui/src/components/cart/cart-summary.tsx +++ b/composable-ui/src/components/cart/cart-summary.tsx @@ -1,6 +1,6 @@ import { useIntl } from 'react-intl' import { useRouter } from 'next/router' -import { useCart } from 'hooks' +import { CartData, useCart } from 'hooks' import { Price } from 'components/price' import { CouponForm } from 'components/forms/coupon-form' import { @@ -15,24 +15,24 @@ import { import { CartSummaryItem } from '.' import { CartPromotions } from './cart-promotions' -export interface CartSummaryProps { +interface CartSummaryProps { rootProps?: StackProps renderCheckoutButton?: boolean + cartData?: CartData } export const CartSummary = ({ rootProps, renderCheckoutButton = true, + cartData, }: CartSummaryProps) => { const router = useRouter() const { cart } = useCart() const intl = useIntl() + const _cartData = cartData ?? cart - const vouchers = - cart.redeemables?.filter((redeemable) => redeemable.object === 'voucher') || - [] const promotions = - cart.redeemables?.filter( + _cartData.redeemables?.filter( (redeemable) => redeemable.object === 'promotion_tier' ) || [] @@ -49,39 +49,39 @@ export const CartSummary = ({ - {cart.summary?.subtotalPrice && ( + {_cartData.summary?.subtotalPrice && ( )} - {cart.summary?.shipping && ( + {_cartData.summary?.shipping && ( )} - {cart.summary?.taxes && ( + {_cartData.summary?.taxes && ( )} - {cart.summary?.totalPrice && ( + {_cartData.summary?.totalPrice && ( <> - + )} - - - - {cart.summary?.totalDiscountAmount && ( + {_cartData.summary?.totalDiscountAmount && ( )} - {cart.summary?.grandPrice && ( + {_cartData.summary?.grandPrice && ( <> - + From a88bb63a890e343726b0df2933edc108f5bd3f3f Mon Sep 17 00:00:00 2001 From: weronika-kurczyna <117282008+weronika-kurczyna@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:27:34 +0100 Subject: [PATCH 49/49] Fix type name --- packages/voucherify/src/validate-discounts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/voucherify/src/validate-discounts.ts b/packages/voucherify/src/validate-discounts.ts index 132e0de..e3e78c2 100644 --- a/packages/voucherify/src/validate-discounts.ts +++ b/packages/voucherify/src/validate-discounts.ts @@ -17,7 +17,7 @@ type ValidateDiscountsParam = { voucherify: ReturnType } -export type validateCouponsAndPromotionsResponse = { +export type ValidateCouponsAndPromotionsResponse = { promotionsResult: PromotionsValidateResponse validationResult: ValidateStackableResult } @@ -30,7 +30,7 @@ export type ValidateStackableResult = export const validateCouponsAndPromotions = async ( params: ValidateDiscountsParam -): Promise => { +): Promise => { const { cart, codes, voucherify } = params const order = cartToVoucherifyOrder(cart)