From c95ac407ee1a5efa8fe61fa5c08c7783cb653dbf Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Tue, 5 May 2026 15:58:42 +0200 Subject: [PATCH 1/2] refactor(payments): cleanup --- .env.e2e.example | 5 - .env.template | 7 - .github/workflows/sonarcloud-analysis.yml | 1 - .github/workflows/tests.yaml | 1 - src/config.ts | 6 - src/controller/payments.controller.ts | 205 ------------------ src/services/payment.service.ts | 182 +--------------- src/services/users.service.ts | 19 -- .../controller/payments.controller.test.ts | 85 +------- tests/src/services/payment.service.test.ts | 39 ---- tests/src/services/users.service.test.ts | 40 ---- 11 files changed, 5 insertions(+), 585 deletions(-) diff --git a/.env.e2e.example b/.env.e2e.example index d9516259..c4a7cd70 100644 --- a/.env.e2e.example +++ b/.env.e2e.example @@ -1,14 +1,9 @@ NODE_ENV=development SERVER_PORT=8003 MONGO_URI=mongodb://admin:password@payments-mongo:27018/payments -STORAGE_GATEWAY_SECRET= -STORAGE_GATEWAY_URL= STRIPE_SECRET_KEY=get_it_from_stripe_dev_section STRIPE_WEBHOOK_KEY= JWT_SECRET=38FTANE5LY90NHYZ -DRIVE_GATEWAY_URL=localhost:3000 -DRIVE_GATEWAY_USER=user -DRIVE_GATEWAY_PASSWORD=gatewaypass REDIS_HOST=payments-redis:6379 REDIS_PASSWORD= DRIVE_NEW_GATEWAY_URL=http://drive-server-wip:3004/api diff --git a/.env.template b/.env.template index cb64ca38..7744399c 100644 --- a/.env.template +++ b/.env.template @@ -1,17 +1,11 @@ NODE_ENV=development SERVER_PORT=8003 MONGO_URI=mongodb://admin:password@payments-database:27017/payments -STORAGE_GATEWAY_SECRET=gateway_secret -STORAGE_GATEWAY_URL=gateway_url STRIPE_SECRET_KEY=get_it_from_stripe_dev_section STRIPE_WEBHOOK_KEY=webhook_key JWT_SECRET=38FTANE5LY90NHYZ -DRIVE_GATEWAY_URL=http://drive-server:8000 -DRIVE_GATEWAY_USER=user -DRIVE_GATEWAY_PASSWORD=gatewaypass REDIS_HOST=payments-cache REDIS_PASSWORD= -COMEBACK_COUPON_CODE= DRIVE_NEW_GATEWAY_URL=http://drive-server-wip:3004/api DRIVE_NEW_GATEWAY_SECRET=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBNG5lZS9GOTgvME4yYWxTSkNyZzB5bzJRRysydzR5SGk3ZXVDT3JYYUhENzFmN0NrClhMVnR5cVkxUUVjRVZxbkJuUFJ1aUdRL0pJc1pCSlV4aEE5TmdwdUpIbVY0aytnMEorRGcxeS9wd3k4L0lNM0EKYU5zbVlqeHNDWUFGQUloalA2TWZQbFUzU2FncnpFUTZJRVNzeHBzT1JhRXd3WUZIWm42TU50b0FGbktMb3VlMQpaa1FaUkpVcDRmUkpTL3Bja3VTUjY2RXIxSzI4WHJKYnhhOXpCNG9SbFJMb0ExQ2cvTFN6ZEFQc2lVMzlSOWtlCmpsQSsxTjJMS1VhazVSdzljeU5DNDd5R0t2YnErdk0zWGlBZk43Wk1teTdkY09aeXcyZW9idFFUVzVtTmR1WEkKVjh4VnllMzJKcmNwbmFlamw5VDBuU1hiRFhPTldnejdJVnJqNFFJREFRQUJBb0lCQVFDeXBqQzUzODNvUkZ5KwpobzlROEgxY3FBM2Rxa1RXK0YxZTJHRDBWWTZJcDdYY2xBa2t4VTZtQlRXT3pqY0M4b2swZXJKVFQ5bHJ4M3JsCjNaZWhHTDFKWWM3cU5wdkcrZTlpNGdnY1dNU3NYN0lKKzZWa1VqVFdXOW5TS0xaSmRFM3Uzb3lBREpNL3ZMVkkKUHk0blZHV0Rpci9ZZDg3UloxMWU0a2RUNGVjZHpLUnlWSlp0d2txTENWeWVkMTgrOUlxdDdmSzZOaHhQM1ZldApQMGl2K1NvWjZ2L3Z4K1ppSVZia05YUHVVZWdnc2lTRitXT01xeHY0RkFvcFVkY2lQU0JZendrME8vekpRTkV4Ckk5RDlaUy9NZjVtZjhLYnU5Q3RCaENhOEtNdnlxM2pzTGxQOEl0QTdrdS9lZWh3SFVlM2tucU5ZTDlRUjNkMHEKU1MrMHpJQVJBb0dCQVBsV2VOeW9UZ2pxVHY1YlZQOSs4Z2ZUbyt3N1N5cWNVcDZrVjFEaTJWaVNPb3FtWGp3aQo0amYwdS9lV1AvZVFtMkMvSnBLSW1VK2RxdWZsMmpxSElLNVJOTGI3V3d2QXdjTU03eThsNFAxTGdMY3hJWG5DCm9GWTJiVHBwbmhwL3I2VnVaQm5tak5hU1B0M1M1TjRYTUpCTkxuTFBCRHdBU2tyNjQ4T0d3RzFsQW9HQkFPaUUKdFpmbHRJcDhWU2ZiZzlBQVYvRWxQRWJLK2lSM2ZNUDF1MjJ2MXMrakZZTmlGUVEreXdNOWpzR0t4bXoxaEhDNgo5VUpZV0pEamxTcVJZUFIxK2oweVNSZ09OTWRhazdQSmNuWFF4akMwaDZ4SW55b042bzJGY0k2bW1hbzZZWU5CCjA2QisrR1hsdU5BblhXYll5YnZUaUorY1Q1OG01cEJxVkpSaWZvTE5Bb0dBSUpNTmd6Wnh5M1JoRVpYNUN0QVkKNnJEWnI1a25mcytoYzV4ZzArNXZHc1V2NU1GTGVtdk1SaWN4RDIyUHVkWW9sb0VpbHU1RnFVTWQrdUhxbXM3ZwpsQ1dEejR3VEh2djExSGV5SCtUQStoYU5JR0hJejlGL2hRUGpUVWhUSVg4aEFXbmtwZ1dhek1XYWRQeUNiZ2wxCmpNU29sdE00NkdYWlR1WnNMelZCbW9rQ2dZRUFodm5pNEpaN3c1aUJabTNMaXNkb1JaZ3o4WFNLMlBoeitOMkIKUEI5RE42MllJM2lnY3FKdy95U1E1bEZFOXFOZmlvRTlOcEpLZDNGbGVoeUNoK2FrcVRtenJMOHliRGRzWG9XbQphRFlWbHRoMW1kRmVjKzg0SlljODI5SmlpYXJ5U0Z2Q0dmMGEvU3ZwcVF2dzhHOFFUSFJ6YnhReU5GVkVxcmpPCitJUmtwRkVDZ1lFQXRodTR4YTloWDZVK2hNZm9ZeWRrMTFibnU3OTNuM211eW42M1lmSDZtb2REbWtqbzJBdzYKYWR1RTViakx2UUlWMHNUR1JRdktKbzBBVFlqU1NnZFhNY0xwY25ZWm1IN21Yd2Q1c0k4cytTVXl3aUpleFhoegpjR2lJUWU0bUxFYmVoclVRRXNZb2xJZ1RWYSt2MmVrWHQvUDRuYWtxMitMMnZrbW9zeFdCclZnPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ== PAYMENTS_GATEWAY_SECRET=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS1FJQkFBS0NBZ0VBejFzaEhZbDcrY2lnWVZGYjZRU3FjKy91NGFNeUYrN292N3lNbXMwRm5udWVwaldoCnVTU1h4dHpENHNNcW9ldzR6dmpta0hSYVppSTR3VFhKMitaVWlROFJmOXVnaE5FUG5WckFlVlNSb0dzN2xqcnQKZzdaOENBOUN1ZWY5a0xzbGlKbHcvY2hTRE8xOWNYeUE0aVd6VHh6aDRpMUpkaEZJRVRiQW9wSGF5VUVYUnFOTQpTSWpZcG01bzA4TUh2SWFVNmFEanBUc0FyQnNhLzA4VGE0R1dyWnVEUFovVVhnMTBVcEZTb3RTS3B4MHp4UHRxCklSR0F3V3R1a21hSUMvbDNsTW5YWUxLMGhiczUxOVlFV0p6cjRncFE5UXVMaDNWaDZOQzYwUGdWTW50NkliS3YKMTI2SkdtN1BKNWJSdVJDSTgzYlVHZGFKd0dQaTRvQm03WlZnNGRFaGw4cWhwRzVMcWp1NEQwWnF4SHBaek9zdgp5K0Y4eWloNTdFTFA5dWVIZTQ1NEhrQXI5NjJGaGFMRTJ2MkRyM3VubjNhYWNoNjBMcDM2cERBVEljakVFL0FNCjdHYklwV0RiWTFQYTV4YTQ1Q3VDMkFsL3RlNUh2blUwMnNwcjM3OHJMdnpMeGJRZXkvL2N4RkMzc094MzIrU1gKR3NFUkxPOUo4U09YREJhSmFIZ0djSnZKZ2NkVVpUbmlDMDBISlZKZm5MUGpTa2M5QmFYZ2swaW4wWStOY2NaRQo2VU41RmF0My9jb0lWVmJ0UnErVkdPT2tRR3JSYk9sZEVHNVRLaGNraGxkbFBZWktDUTFDejRYUzBwMlFCMWZjCnhzcGI1MVJEUHZ1ckZCTFRaUTUzYnVNMVdKN1dwNm55Sm1oM0ZMUzdlWnhjbjBUTzkxSzAvUjcrZUowQ0F3RUEKQVFLQ0FnQkhSVzFtZDZFVFEvQk1RdWl5ZVJZVmIzek9OWWU4VGpQVjQzcjRva3V5STQ5dVZiVFdyRXMyNFI2NwpUSlhVdmhyd21RQzIyaWRRUDZiK1VmeW1Cczg3cE9CQThENkdLRTJUcW1QSjBGV1ZyQVg1Snh3SVQ2cm1Ja2l2CkdaeEFLUE5IdG1YdDlQS3UydHBwTFlBbk93b2N3VEtxeVNJYzRPZkNjdTFYYzRhZDhpK0w2Z2tJVFhFYUU0MFkKYkVxMmRCT3ZFY3Z0T1JDTUYyejZJRFh1bDhZd3Vla3NQMG1CWW1KL0Z0QXVnNXQ4d1Q3UUVCSjBkMDJvTGNMOQpzeHhEOFdVMjBRR2tqUWdiaHpUa2lQMUpiL1pyNVZ2YVBqT2hSYWwzbW5iNzZLbHdocHgrQktzSUEwaEF1NkdjClcreU9EYTdrOEEwTExJQ0FqbFNud2JhZkp0NzdZSFl1VGUzdW5ISFY0dS95alIxSDdsMEFOUGVHZkY3aUowVG4KTlVpck1WN3RlKzZGcWQ4b01QOVJrd1NXRjIrMHVnZCs5YjBicEkwTEpQVElNb2NRT2VSSmVvc3ArYld6RkpzbAorY2ZUUFFBVHBVSDZ4MC8za0RCVnhPUUhiKzVjS1h3ajB4czRabTlXMThTU1p6aFh1ZngrUkE3WTgydHpLNHVaCk1LOHRvMGxjb0JRYzBqbkgyM2FUV0kvUDlRNkpEUjF4SzFLY09lMUhDbElvZDM2UHZId2dWbnZmMUpoNUhOMnAKSk96elVpSUpkcHBIZ1BhOWJXaWRsZTV3V1crTzRTZlR4Wnd4U2E5b0xoUWJmdDJsRTZaSFlxNjBYcnFaY0pZYgpmYmtTNDRpT09MVGQ0SVdnZHRWdG1TeStQUXhEQ1pLV1V5ZUorNjRySWgxaXBGc3NnUUtDQVFFQTlQQWJHcjFqCnEwT21VdHhRcWJqdzVOQ3lpL1FJYXd3cU5ST09YTkd5cUJ6NUhSVzFKUG1tV0dsV0U0Ykt2anQ3RDdzd2ZIQ3oKOFhoQmlCMnBQRVhUaDVzZmFkSzErYTY5SjdabGlzK05HYTFpKzd3REdxQ0puL1AzOTFvYlJRUU1SbWJiR2tSKwpyZ3lqQmFXMHR1MWpLK0NZQXpwc0xJc0dOL3BmQWxkbk9XbUJuL2pSeUwvSlYzRkRjS2Zzay9oSjJJOEZETlFmCitSR2gwR0Y2bUZMTzV5M0RIUVpvUnZOeFhmRXlXQTkyZ2h2UnVFMUZyQ2h5RjFsOUtXZXBmS25KeFR1K0o0N3cKcklXNkhPcVBsZHBtVng2cGVhVmtFbTZMSEM3Qk5heTRSaXVDTXB2ZDFLbjZvVU1OM1R1dGxVWHhCK3o4MS95NQpEblc0Nm43UktleHVEUUtDQVFFQTJMaURXSE1JUUthcGRnSllRQUdwb1VEWEVpbFdGeXA1N0VZS1VoaGF1ZjVtCk9lRWhodmd5dkxGcmVGb3F2OUdJaGVDOVNjNUR0eFJWekp1SGtFZGdTbTE5dTBzQTd5bHg2ZTRwWkd5Tm1PaFAKOW1tSEVoSXE2dTduT3doY3NkSW5FbVFJRVlzZjBkRzdFWlppdVdBZjI2NkRyTmcvNklFVGJpakRlTklFV0IvSwpBY0RKQVk3WVQrYjJ5c0pyOG5kMHo4VjVlMnZuNkRCVmZ4RHJLbjJGMFFtN3hOdWE2a29wbmgybUhmYXZoUFJyClRIa09OVFU2SVI5bEJ0blozVjJNT1k3bVd1TUV0WkxyVEtyM2EzMjdHNzNvTzZmSzhFNjdDRDF5MTdGN0xvTC8KMHdhKzR0R2lpVzVkZzhWR2dUT2plOVJJMVdhQUpOaG5xRk0yRFZNZzBRS0NBUUFhZ1hFdGI1azlpMUNRWHU4TApyc3ZDdHlMYktrbE83RDRWQ0V5N2xxV3lzNC94cWN1MGVKK2JxYXA2Nm5jK0pzbW1aaWRWRUEwbzhFNkhJRTZVCkN5cGMzbGlENXgxeUs0cWtwWVJQaFM1THZRdWRHamRyeGp1ZVo1UkozQ0pmVVpUU2VZYjBUTDA0c0gxanV3N1gKVE5FU2luZG93Z1c2dkVDc0JoZ0o5Tk5penh3TDU2MHNBRFVWbFZncTlNRVJNaWtybWk1OXVPYk0yUytka1M3bgpGTzcyN1dqVDEwR3BpK2FVdCtrdXhsMllydEgvRTcyYVo3WVErTW9tbE9VdWJHRTlTcjNGSWg3QlRLZGJRYmJRCkRKWk9qeTdmenhvSk5KVXhNNDRNOU9mc0VBRkM0TU1jcEZoTzR5YTQ3aUJXcXY3dVVLTDc3SWxLRzBzcmRSWEYKWjZSeEFvSUJBUUNQVHd3RHpDVno0d0dDT0xDMHVxUzZMZzlLUWkxY3FranZkYTFUZGlsZ2ZwcUl3WmVURWNNegpSYnRFWFpPUlBuU2gydWd3eXdXNkplZEtvcm8xTFEyK2ljS3Z2RHhFNmtvYW45T2RSYThvb3M1bHFvaVg2WTJaCnh4cit6VnZHZHFwQm5nWTcyNXpSK1hkVGZQZVJqNy9oVy9oVXJyY3IvMWFpN0srOTBGcnhEeXhjbG1nVThLbVgKeEtvRGtDY2pzZHg3bkNEbC9ZZmY0VDVZQlE4TkRPNFZPZk9CakpwWFBXMWgwa2RMM3hsWHVPelBKK20rUTVGSgozK0hGY3Y5L1EySDdtY1EzNjVEc3BOZVYzaVE0WDI4QVBFYzZVNDE5OVFIMncvT1NNQm05dXdDQ1FoNnVER1FICnJ3U0ZvMGtwSE1XSmwrb0Z1MzhYWGtiRlp5a0NES3RSQW9JQkFRQ2NraHBGQ0thRWpHZE9rWEtkUDdlai90ZDkKSXFGTEV1YWgzaExkTEMyYlBrVXFlc2ZvY2JBSzZPV2t4MFk3c0h0VUxFanBYSUVuSXhwYXVQZXJlTUdzZ204dwo2azJjWXMxemVLL3RHR0RlcDZZb0tVVCtrblNLV0g5cG5SNVBnb0lrK1JGVkZuU3hpWEd3MVdhODBINzRYdFdTCjFKV2hLVUM1bVpobUZ2NFBSWjZ2a0Z1UmtkckZ5cmxFbjhva1dpemZYNWtiMmN1ZGk2cUllcThyeDhFejlLdmEKUGIwL0JMR3YzWlVaOGMrUnk1RVNzL0laZGtqOG9KeEpJMUpLWmE4cEJDYVUwU2xyTHVPWDdhNDJ0SmNWMzFMdQpna2lTelNUZjk2YWhyRm5lQTF3Y2tYckROZWtGM09mMXFxQVU4QUFIWURDSlBVQXNiOWF0K25nM21FK3IKLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K @@ -23,6 +17,5 @@ CRYPTO_PAYMENTS_PROCESSOR_SECRET_KEY=secret-key CRYPTO_PAYMENTS_PROCESSOR_API_KEY=api-key CRYPTO_PAYMENTS_PROCESSOR_API_URL=https://api-url.com VPN_URL=https://api-url.com -PC_CLOUD_TRIAL_CODE=your_code DRIVE_WEB_URL= CHART_API_URL= \ No newline at end of file diff --git a/.github/workflows/sonarcloud-analysis.yml b/.github/workflows/sonarcloud-analysis.yml index 08b30736..da4ef387 100644 --- a/.github/workflows/sonarcloud-analysis.yml +++ b/.github/workflows/sonarcloud-analysis.yml @@ -37,7 +37,6 @@ jobs: - run: echo PAYMENTS_GATEWAY_PUBLIC_SECRET=${{secrets.PAYMENTS_GATEWAY_PUBLIC_SECRET}} >> ./.env - run: echo KLAVIYO_BASE_URL=test_klaviyo_key_123 >> ./.env - run: echo KLAVIYO_API_KEY=test_klaviyo_key_123 >> ./.env - - run: echo PC_CLOUD_TRIAL_CODE=my_code >> ./.env - run: echo "registry=https://registry.yarnpkg.com/" > .npmrc - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index acfc8ee9..2b0bf1eb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -35,7 +35,6 @@ jobs: - run: echo CHART_API_URL=api_url >> ./.env - run: echo PAYMENTS_GATEWAY_SECRET=${{secrets.PAYMENTS_GATEWAY_SECRET}} >> ./.env - run: echo PAYMENTS_GATEWAY_PUBLIC_SECRET=${{secrets.PAYMENTS_GATEWAY_PUBLIC_SECRET}} >> ./.env - - run: echo PC_CLOUD_TRIAL_CODE=my_code >> ./.env - run: echo KLAVIYO_BASE_URL=test_klaviyo_url >> ./.env - run: echo KLAVIYO_API_KEY=test_klaviyo_key_123 >> ./.env diff --git a/src/config.ts b/src/config.ts index 4ad07d24..a6edb3e6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,14 +4,9 @@ const BASE_REQUIRED_VARIABLES = [ 'NODE_ENV', 'SERVER_PORT', 'MONGO_URI', - 'STORAGE_GATEWAY_SECRET', - 'STORAGE_GATEWAY_URL', 'STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_KEY', 'JWT_SECRET', - 'DRIVE_GATEWAY_URL', - 'DRIVE_GATEWAY_USER', - 'DRIVE_GATEWAY_PASSWORD', 'DRIVE_NEW_GATEWAY_URL', 'DRIVE_NEW_GATEWAY_SECRET', 'PAYMENTS_GATEWAY_PUBLIC_SECRET', @@ -22,7 +17,6 @@ const BASE_REQUIRED_VARIABLES = [ 'CRYPTO_PAYMENTS_PROCESSOR_SECRET_KEY', 'CRYPTO_PAYMENTS_PROCESSOR_API_KEY', 'VPN_URL', - 'PC_CLOUD_TRIAL_CODE', 'CHART_API_URL', 'DRIVE_WEB_URL', 'RECAPTCHA_V3_ENDPOINT', diff --git a/src/controller/payments.controller.ts b/src/controller/payments.controller.ts index 07319926..b5674de6 100644 --- a/src/controller/payments.controller.ts +++ b/src/controller/payments.controller.ts @@ -4,12 +4,9 @@ import jwt from 'jsonwebtoken'; import { type AppConfig } from '../config'; import { UsersService } from '../services/users.service'; import { - CouponCodeError, IncompatibleSubscriptionTypesError, InvalidSeatNumberError, - ExistingSubscriptionError, MissingParametersError, - NotFoundPlanByIdError, NotFoundPromoCodeByNameError, PromoCodeIsNotValidError, } from '../errors/PaymentErrors'; @@ -133,132 +130,6 @@ export function paymentsController( }, ); - fastify.post<{ - Querystring: { trialToken: string }; - Body: { - customerId: string; - priceId: string; - currency: string; - token: string; - trialCode: string; - }; - }>( - '/create-subscription-with-trial', - { - schema: { - body: { - type: 'object', - required: ['customerId', 'priceId'], - properties: { - customerId: { - type: 'string', - }, - priceId: { - type: 'string', - }, - token: { - type: 'string', - }, - currency: { - type: 'string', - }, - trialCode: { - type: 'string', - }, - }, - }, - }, - }, - async (req, res) => { - const { customerId, priceId, currency, token } = req.body; - - if (!customerId || !priceId) { - throw new MissingParametersError(['customerId', 'priceId']); - } - - try { - const payload = jwt.verify(token, config.JWT_SECRET) as { - customerId: string; - }; - const tokenCustomerId = payload.customerId; - - if (customerId !== tokenCustomerId) { - return res.status(403).send(); - } - } catch (error) { - return res.status(403).send(); - } - - const { trialToken } = req.query; - - if (!trialToken) { - return res.status(403).send('Invalid trial token'); - } - - try { - const payload = jwt.verify(trialToken, config.JWT_SECRET) as { trial?: string }; - if (!payload.trial || payload.trial !== 'pc-cloud-25') { - throw new Error('Invalid trial token'); - } - } catch { - return res.status(403).send('Invalid trial token'); - } - - try { - const subscriptionSetup = await paymentService.createSubscriptionWithTrial( - { - customerId, - priceId, - currency, - }, - { - name: 'pc-cloud-25', - }, - ); - - return res.send(subscriptionSetup); - } catch (err) { - const error = err as Error; - req.log.error(`[ERROR CREATING SUBSCRIPTION WITH TRIAL]: ${error.stack ?? error.message}`); - - if (error instanceof MissingParametersError) { - return res.status(400).send({ - message: error.message, - }); - } else if (error instanceof PromoCodeIsNotValidError) { - return res - .status(422) - .send({ message: 'The promotion code is not applicable under the current conditions' }); - } else if (error instanceof ExistingSubscriptionError) { - return res.status(409).send({ - message: error.message, - }); - } - - return res.status(500).send({ - message: 'Internal Server Error', - }); - } - }, - ); - - fastify.get<{ - Querystring: { code: string }; - }>('/trial-for-subscription', async (req, rep) => { - const { code } = req.query; - if (!code || code !== process.env.PC_CLOUD_TRIAL_CODE) { - return rep.status(400).send(); - } - - return jwt.sign({ trial: 'pc-cloud-25' }, config.JWT_SECRET); - }); - - fastify.get('/users/exists', async (req, rep) => { - await assertUser(req, rep, usersService); - - return rep.status(200).send(); - }); - fastify.get<{ Querystring: { limit: number; starting_after?: string; userType?: UserType; subscription?: string }; }>( @@ -566,82 +437,6 @@ export function paymentsController( }, ); - fastify.get('/request-prevent-cancellation', async (req) => { - const { uuid } = req.user.payload; - try { - const user = await usersService.findUserByUuid(uuid); - - return paymentService.isUserElegibleForTrial(user, { - name: 'prevent-cancellation', - }); - } catch (err) { - const error = err as Error; - req.log.error( - `[REQUEST-PREVENT-CANCELLATION/ERROR]: Error for user ${uuid} ${error.message}. ${error.stack || 'NO STACK'}`, - ); - throw err; - } - }); - - fastify.put('/prevent-cancellation', async (req, rep) => { - const { uuid } = req.user.payload; - const user = await usersService.findUserByUuid(uuid); - - try { - await paymentService.applyFreeTrialToUser(user, { - name: 'prevent-cancellation', - }); - return rep.status(200).send({ message: 'Coupon applied' }); - } catch (err) { - if (err instanceof CouponCodeError) { - return rep.status(403).send({ message: err.message }); - } else { - req.log.error(err); - return rep.status(500).send({ message: 'Internal server error' }); - } - } - }); - - fastify.get<{ - Querystring: { planId: string; currency?: string }; - schema: { - querystring: { - type: 'object'; - properties: { planId: { type: 'string' }; currency: { type: 'string' } }; - }; - }; - config: { - rateLimit: { - max: 5; - timeWindow: '1 minute'; - }; - }; - }>( - '/plan-by-id', - { - config: { - skipAuth: true, - }, - }, - async (req, rep) => { - const { planId, currency } = req.query; - - try { - const planObject = await paymentService.getPlanById(planId, currency); - - return rep.status(200).send(planObject); - } catch (error) { - const err = error as Error; - if (err instanceof NotFoundPlanByIdError) { - return rep.status(404).send(err.message); - } - - req.log.error(`[ERROR WHILE FETCHING PLAN BY ID]: ${err.message}. STACK ${err.stack ?? 'NO STACK'}`); - return rep.status(500).send({ message: 'Internal Server Error' }); - } - }, - ); - fastify.get<{ Querystring: { priceId: string; promotionCode: string }; schema: { diff --git a/src/services/payment.service.ts b/src/services/payment.service.ts index 12a72af8..21a4d545 100644 --- a/src/services/payment.service.ts +++ b/src/services/payment.service.ts @@ -3,12 +3,11 @@ import dayjs from 'dayjs'; import { DisplayPrice } from '../core/users/DisplayPrice'; import { ProductsRepository } from '../core/users/ProductsRepository'; -import { User, UserSubscription, UserType } from '../core/users/User'; +import { UserSubscription, UserType } from '../core/users/User'; import { Bit2MeService } from './bit2me.service'; -import { BadRequestError, InternalServerError, NotFoundError } from '../errors/Errors'; +import { BadRequestError, NotFoundError } from '../errors/Errors'; import { NotFoundSubscriptionError, - CouponCodeError, InvalidSeatNumberError, IncompatibleSubscriptionTypesError, CustomerNotFoundError, @@ -38,21 +37,10 @@ import { CustomerSource, Customer as StripeCustomer, } from '../types/stripe'; -import { PaymentIntent, PromotionCode, PriceByIdResponse, Reason } from '../types/payment'; -import { - RenewalPeriod, - PlanSubscription, - SubscriptionCreated, - RequestedPlan, - HasUserAppliedCouponResponse, -} from '../types/subscription'; +import { PaymentIntent, PromotionCode, PriceByIdResponse } from '../types/payment'; +import { RenewalPeriod, PlanSubscription, SubscriptionCreated, RequestedPlan } from '../types/subscription'; import { stripePaymentsAdapter } from '../infrastructure/adapters/stripe.adapter'; -const reasonFreeMonthsMap: Record = { - 'prevent-cancellation': 3, - 'pc-cloud-25': 6, -}; - export class PaymentService { constructor( private readonly provider: Stripe, @@ -121,30 +109,6 @@ export class PaymentService { }; } - async createSubscriptionWithTrial( - payload: { - customerId: string; - priceId: string; - seatsForBusinessSubscription?: number; - currency?: string; - promoCodeId?: Stripe.SubscriptionCreateParams['promotion_code']; - companyName?: string; - companyVatId?: string; - }, - trialReason: Reason, - ) { - const now = new Date(); - const trialEnd = Math.floor(now.setMonth(now.getMonth() + reasonFreeMonthsMap[trialReason.name]) / 1000); - - const subscription = await this.createSubscription({ - ...payload, - trialEnd, - metadata: { 'why-trial': trialReason.name }, - }); - - return subscription; - } - async createSubscription({ customerId, priceId, @@ -640,37 +604,6 @@ export class PaymentService { return updatedSubscription; } - /** - * Function to update the subscription with a freeTrial (prevent-cancellation flow) - * @param customerId - The customer id - * @param priceId - The price id - * @param reason - The reason to update the subscription - * @returns The updated subscription with the corresponding trial_end - */ - async updateSubscriptionByReason(customerId: CustomerId, priceId: PriceId, reason: Reason) { - let trialEnd = 0; - - const { data } = await this.provider.subscriptions.list({ - customer: customerId, - status: 'active', - }); - const [lastActiveSub] = data; - - if (reason.name in reasonFreeMonthsMap) { - const date = new Date(lastActiveSub.current_period_end * 1000); - trialEnd = date.setMonth(date.getMonth() + reasonFreeMonthsMap[reason.name]); - } - - return this.updateIndividualSub({ - customerId: customerId, - priceId: priceId, - additionalOptions: { - trial_end: trialEnd === 0 ? undefined : Math.floor(trialEnd / 1000), - metadata: { reason: reason.name }, - }, - }); - } - /** * Function to update the subscription price (change the plan) * @param customerId - The customer id @@ -845,41 +778,6 @@ export class PaymentService { return invoicesMapped; } - async isUserElegibleForTrial(user: User, reason: Reason): Promise { - const { lifetime, customerId } = user; - - if (lifetime) { - return { - elegible: false, - }; - } - - const userSubscriptions = await this.provider.subscriptions.list({ - customer: customerId, - status: 'all', - }); - - const isFreeTrialAlreadyApplied = userSubscriptions.data.some( - (invoice) => invoice.metadata && invoice.metadata.reason === reason.name, - ); - - return isFreeTrialAlreadyApplied ? { elegible: false } : { elegible: true }; - } - - async applyFreeTrialToUser(user: User, reason: Reason) { - const { customerId } = user; - const hasCouponApplied = await this.isUserElegibleForTrial(user, reason); - if (hasCouponApplied.elegible) { - const subscription = await this.findIndividualActiveSubscription(customerId); - - await this.updateSubscriptionByReason(customerId, subscription.items.data[0].plan.id, reason); - - return true; - } else { - throw new CouponCodeError('User already applied coupon'); - } - } - getSetupIntent(customerId: string, metadata: Stripe.MetadataParam): Promise { return this.provider.setupIntents.create({ customer: customerId, @@ -1197,78 +1095,6 @@ export class PaymentService { }); } - /** - * Deprecated - Use getPriceById instead - * @param priceId - Id of the price we want to fetch - * @param currency - Currency if needed - * @returns - The selected plan and their upsell if needed - */ - async getPlanById(priceId: PlanId, currency?: string): Promise { - let upsellPlan: RequestedPlan['upsellPlan']; - let businessSeats; - const currencyValue = currency ?? 'eur'; - - try { - const prices = await this.getPricesRaw(currencyValue); - - const price = prices.find((price) => price.id === priceId); - - if (!price) { - throw new NotFoundPlanByIdError(priceId); - } - - const { id, currency, metadata, type, recurring, product: productId } = price; - - const isBusinessPrice = !!metadata.type && metadata.type === 'business'; - - if (isBusinessPrice) { - businessSeats = { - minimumSeats: Number(metadata.minimumSeats), - maximumSeats: Number(metadata.maximumSeats), - }; - } - - const selectedPlan: RequestedPlan['selectedPlan'] = { - id: id, - currency: currencyValue, - amount: price.currency_options![currencyValue].unit_amount as number, - bytes: parseInt(metadata?.maxSpaceBytes), - interval: type === 'one_time' ? 'lifetime' : (recurring?.interval as 'year' | 'month'), - decimalAmount: (price.currency_options![currencyValue].unit_amount as number) / 100, - type: isBusinessPrice ? UserType.Business : UserType.Individual, - ...businessSeats, - }; - - if (recurring?.interval === 'month') { - const upsell = await this.getUpsellProduct(productId as string, currency); - - if (upsell?.active) { - upsellPlan = { - id: upsell.id, - currency: currencyValue, - amount: upsell.currency_options![currencyValue].unit_amount as number, - bytes: parseInt(upsell.metadata?.maxSpaceBytes), - interval: upsell.type === 'one_time' ? 'lifetime' : (upsell.recurring?.interval as 'year' | 'month'), - decimalAmount: (upsell.currency_options![currencyValue].unit_amount as number) / 100, - type: isBusinessPrice ? UserType.Business : UserType.Individual, - ...businessSeats, - }; - } - } - - return { - selectedPlan, - upsellPlan, - }; - } catch (err) { - const error = err as Error; - if (error instanceof NotFoundPlanByIdError || error.message.includes('No such price')) { - throw new NotFoundPlanByIdError(priceId); - } - throw new InternalServerError(); - } - } - /** * This function is used to get the promotion code object from Stripe. * @param promoCodeName - The name of the promotion code diff --git a/src/services/users.service.ts b/src/services/users.service.ts index 4fa3b6d0..c7f8c393 100644 --- a/src/services/users.service.ts +++ b/src/services/users.service.ts @@ -121,25 +121,6 @@ export class UsersService { }); } - /** - * Indicates if the coupon has been used or not by a given user - * @param user User that could have been used a coupon - * @param couponCode The coupon code that could have been used - * @returns A boolean indicating if the coupon has been used or not - */ - async isCouponBeingUsedByUser(user: User, couponCode: Coupon['code']): Promise { - const coupon = await this.couponsRepository.findByCode(couponCode); - const isTracked = !!coupon; - - if (!isTracked) { - return false; - } - - const userCouponEntry = await this.usersCouponsRepository.findByUserAndCoupon(user.id, coupon.id); - - return !!userCouponEntry; - } - /** * @description Retrieves the unique coupon codes associated with a given user. * diff --git a/tests/src/controller/payments.controller.test.ts b/tests/src/controller/payments.controller.test.ts index 5c08061b..078424bd 100644 --- a/tests/src/controller/payments.controller.test.ts +++ b/tests/src/controller/payments.controller.test.ts @@ -5,7 +5,6 @@ import { getCustomer, getLicenseCode, getPaymentIntent, - getPrices, getUniqueCodes, getUser, getValidAuthToken, @@ -15,7 +14,7 @@ import { } from '../fixtures'; import { closeServerAndDatabase, initializeServerAndDatabase } from '../utils/initializeServer'; import { PaymentService } from '../../../src/services/payment.service'; -import { CustomerNotFoundError, NotFoundPlanByIdError } from '../../../src/errors/PaymentErrors'; +import { CustomerNotFoundError } from '../../../src/errors/PaymentErrors'; import config from '../../../src/config'; import { assertUser } from '../../../src/utils/assertUser'; import { TierNotFoundError, TiersService } from '../../../src/services/tiers.service'; @@ -72,88 +71,6 @@ describe('Payment controller e2e tests', () => { }); }); - describe('Fetching plan object by ID and contains the basic params', () => { - describe('Fetch subscription plan object', () => { - it('When the subscription priceId is valid, then the endpoint returns the correct object', async () => { - const mockedPrice = getPrices(); - const expectedKeys = { - selectedPlan: { - id: expect.anything(), - currency: expect.anything(), - amount: expect.anything(), - bytes: expect.anything(), - interval: expect.anything(), - decimalAmount: expect.anything(), - }, - }; - jest.spyOn(PaymentService.prototype, 'getPlanById').mockResolvedValue(expectedKeys); - - const response = await app.inject({ - path: `/plan-by-id?planId=${mockedPrice.subscription.exists}`, - method: 'GET', - }); - const responseBody = response.json(); - - expect(response.statusCode).toBe(200); - expect(responseBody).toMatchObject(expectedKeys); - }); - - it('When the subscription priceId is not valid, then it returns 404 status code', async () => { - const mockedPrice = getPrices(); - const notFoundPlanByIdError = new NotFoundPlanByIdError('Plan not found'); - jest.spyOn(PaymentService.prototype, 'getPlanById').mockRejectedValue(notFoundPlanByIdError); - - const response = await app.inject({ - path: `/plan-by-id?planId=${mockedPrice.subscription.doesNotExist}`, - method: 'GET', - }); - - expect(response.statusCode).toBe(404); - }); - }); - - describe('Fetch Lifetime plan object', () => { - it('When the lifetime priceId is valid, then it returns the lifetime price object', async () => { - const mockedPrice = getPrices(); - - const expectedKeys = { - selectedPlan: { - id: expect.anything(), - currency: expect.anything(), - amount: expect.anything(), - bytes: expect.anything(), - interval: expect.anything(), - decimalAmount: expect.anything(), - }, - }; - jest.spyOn(PaymentService.prototype, 'getPlanById').mockResolvedValue(expectedKeys); - - const response = await app.inject({ - path: `/plan-by-id?planId=${mockedPrice.lifetime.exists}`, - method: 'GET', - }); - - const responseBody = response.json(); - - expect(response.statusCode).toBe(200); - expect(responseBody).toMatchObject(expectedKeys); - }); - - it('When the lifetime priceId is not valid, then returns 404 status code', async () => { - const mockedPrice = getPrices(); - const notFoundPlanByIdError = new NotFoundPlanByIdError('Plan not found'); - jest.spyOn(PaymentService.prototype, 'getPlanById').mockRejectedValue(notFoundPlanByIdError); - - const response = await app.inject({ - path: `/plan-by-id?planId=${mockedPrice.lifetime.doesNotExist}`, - method: 'GET', - }); - - expect(response.statusCode).toBe(404); - }); - }); - }); - describe('Get the user subscription', () => { describe('The user has a lifetime', () => { it('When the user has a Tier, then the type of subscription is lifetime and the product ID of the tier is returned', async () => { diff --git a/tests/src/services/payment.service.test.ts b/tests/src/services/payment.service.test.ts index 9ccc39ea..b94d3e2f 100644 --- a/tests/src/services/payment.service.test.ts +++ b/tests/src/services/payment.service.test.ts @@ -1,5 +1,4 @@ import dayjs from 'dayjs'; -import { Reason } from '../../../src/types/payment'; import { UserType } from '../../../src/core/users/User'; import { getCharge, @@ -686,44 +685,6 @@ describe('Payments Service tests', () => { }); }); - describe('Creating a subscription with trial', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('When creating a subscription with trial, then it creates the sub with the correct trial end date', async () => { - const fixedDate = new Date('2024-01-01T12:00:00Z'); - jest.setSystemTime(fixedDate); - - const expected = getCreateSubscriptionResponse(); - const payload = { - customerId: getCustomer().id, - priceId: getPrices().subscription.exists, - }; - const trialReason: Reason = { name: 'pc-cloud-25' }; - - const trialMonths = 6; - const expectedTrialEnd = Math.floor( - new Date(fixedDate.setMonth(fixedDate.getMonth() + trialMonths)).getTime() / 1000, - ); - - const createSubSpy = jest.spyOn(paymentService, 'createSubscription').mockResolvedValue(expected); - - const received = await paymentService.createSubscriptionWithTrial(payload, trialReason); - - expect(received).toStrictEqual(expected); - expect(createSubSpy).toHaveBeenCalledWith({ - ...payload, - trialEnd: expectedTrialEnd, - metadata: { 'why-trial': trialReason.name }, - }); - }); - }); - describe('Fetch a price by its ID', () => { it('When the price does not exist, an error indicating so is thrown', async () => { const mockedPrices = getPrice(); diff --git a/tests/src/services/users.service.test.ts b/tests/src/services/users.service.test.ts index 4468a012..8dccd833 100644 --- a/tests/src/services/users.service.test.ts +++ b/tests/src/services/users.service.test.ts @@ -307,46 +307,6 @@ describe('UsersService tests', () => { }); }); - describe('Verify if the user used a tracked coupon code', () => { - it('When the coupon is tracked and used by the user, then returns true', async () => { - const mockedUser = getUser(); - const mockedCoupon = getCoupon(); - (couponsRepository.findByCode as jest.Mock).mockResolvedValue(mockedCoupon); - (usersCouponsRepository.findByUserAndCoupon as jest.Mock).mockResolvedValue({ id: 'entry1' }); - - const result = await usersService.isCouponBeingUsedByUser(mockedUser, mockedCoupon.code); - - expect(couponsRepository.findByCode).toHaveBeenCalledWith(mockedCoupon.code); - expect(usersCouponsRepository.findByUserAndCoupon).toHaveBeenCalledWith(mockedUser.id, mockedCoupon.id); - expect(result).toBe(true); - }); - - it('When the coupon is tracked but not used by the user, then returns false', async () => { - const mockedUser = getUser(); - const mockedCoupon = getCoupon(); - (couponsRepository.findByCode as jest.Mock).mockResolvedValue(mockedCoupon); - (usersCouponsRepository.findByUserAndCoupon as jest.Mock).mockResolvedValue(null); - - const result = await usersService.isCouponBeingUsedByUser(mockedUser, mockedCoupon.code); - - expect(couponsRepository.findByCode).toHaveBeenCalledWith(mockedCoupon.code); - expect(usersCouponsRepository.findByUserAndCoupon).toHaveBeenCalledWith(mockedUser.id, mockedCoupon.id); - expect(result).toBe(false); - }); - - it('When the coupon is not tracked, then returns false', async () => { - const mockedUser = getUser(); - const mockedCoupon = getCoupon(); - (couponsRepository.findByCode as jest.Mock).mockResolvedValue(null); - - const result = await usersService.isCouponBeingUsedByUser(mockedUser, mockedCoupon.code); - - expect(couponsRepository.findByCode).toHaveBeenCalledWith(mockedCoupon.code); - expect(usersCouponsRepository.findByUserAndCoupon).not.toHaveBeenCalled(); - expect(result).toBe(false); - }); - }); - describe('Fetch all coupons linked to a user', () => { it('When the user does not have any coupon associated, then nothing is returned', async () => { const mockedUser = getUser(); From 248c129cc7d057800706f8d0342d2b9bfb2c81a0 Mon Sep 17 00:00:00 2001 From: Xavier Abad <77491413+xabg2@users.noreply.github.com> Date: Tue, 5 May 2026 16:15:09 +0200 Subject: [PATCH 2/2] refactor:remove useless errors --- src/errors/Errors.ts | 6 ------ src/errors/PaymentErrors.ts | 6 ------ src/services/payment.service.ts | 4 ++-- src/types/subscription.ts | 11 ----------- 4 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/errors/Errors.ts b/src/errors/Errors.ts index 63dd0fa3..a6963ddb 100644 --- a/src/errors/Errors.ts +++ b/src/errors/Errors.ts @@ -30,12 +30,6 @@ export class ForbiddenError extends HttpError { } } -export class GoneError extends HttpError { - constructor(message = 'Gone') { - super(message, 410); - } -} - export class InternalServerError extends HttpError { constructor(message = 'Internal Server Error') { super(message, 500); diff --git a/src/errors/PaymentErrors.ts b/src/errors/PaymentErrors.ts index b07f83ae..fc01368b 100644 --- a/src/errors/PaymentErrors.ts +++ b/src/errors/PaymentErrors.ts @@ -6,12 +6,6 @@ export class NotFoundSubscriptionError extends NotFoundError { } } -export class CouponCodeError extends BadRequestError { - constructor(message: string) { - super(message); - } -} - export class InvalidSeatNumberError extends BadRequestError { constructor(message: string) { super(message); diff --git a/src/services/payment.service.ts b/src/services/payment.service.ts index 21a4d545..1ed7e6f3 100644 --- a/src/services/payment.service.ts +++ b/src/services/payment.service.ts @@ -38,7 +38,7 @@ import { Customer as StripeCustomer, } from '../types/stripe'; import { PaymentIntent, PromotionCode, PriceByIdResponse } from '../types/payment'; -import { RenewalPeriod, PlanSubscription, SubscriptionCreated, RequestedPlan } from '../types/subscription'; +import { RenewalPeriod, PlanSubscription, SubscriptionCreated, RequestedPlanData } from '../types/subscription'; import { stripePaymentsAdapter } from '../infrastructure/adapters/stripe.adapter'; export class PaymentService { @@ -995,7 +995,7 @@ export class PaymentService { const { id, metadata, type, recurring } = price; - const selectedPlan: RequestedPlan['selectedPlan'] = { + const selectedPlan: RequestedPlanData = { id: id, currency: currencyValue, amount: price.unit_amount as number, diff --git a/src/types/subscription.ts b/src/types/subscription.ts index 9e479f2c..4c49fcb7 100644 --- a/src/types/subscription.ts +++ b/src/types/subscription.ts @@ -1,6 +1,5 @@ import { DisplayPrice } from '../core/users/DisplayPrice'; import { UserType } from '../core/users/User'; -import { Reason } from './payment'; export enum RenewalPeriod { Monthly = 'monthly', @@ -50,13 +49,3 @@ export type RequestedPlanData = DisplayPrice & { maximumSeats?: number; type?: UserType; }; - -export interface RequestedPlan { - selectedPlan: RequestedPlanData; - upsellPlan?: RequestedPlanData; -} - -export type HasUserAppliedCouponResponse = { - elegible: boolean; - reason?: Reason; -};