From aba2b7eaf5a37b30b29cd175a38867dfda4c8a56 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Thu, 21 Nov 2024 13:39:34 -0500 Subject: [PATCH 01/24] Revert "Squashed commit of the following:" This reverts commit d6c47e7e8115c24b99312553415b07a37983875b. --- website/client/src/components/header/menu.vue | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/website/client/src/components/header/menu.vue b/website/client/src/components/header/menu.vue index 3292277d4cd..f1b04ac4898 100644 --- a/website/client/src/components/header/menu.vue +++ b/website/client/src/components/header/menu.vue @@ -354,15 +354,13 @@ > {{ userHourglasses }} -
+
{{ userGems }} From a7fb8f7d3c5cac4730725a002ecbfe218d5b06e2 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Thu, 21 Nov 2024 14:07:10 -0500 Subject: [PATCH 02/24] refactor(app): Remove front-end Amazon code --- website/client/src/assets/svg/amazonpay.svg | 73 ----- .../src/components/news/birthdayModal.vue | 31 +- .../src/components/payments/amazonModal.vue | 295 ------------------ .../components/payments/buttons/amazon.vue | 135 -------- .../src/components/payments/buyGemsModal.vue | 1 - .../src/components/payments/sendGemsModal.vue | 5 - .../src/components/payments/sendGiftModal.vue | 2 - .../src/components/settings/subscription.vue | 13 - .../client/src/components/static/privacy.vue | 6 - website/client/src/pages/user-main.vue | 3 - 10 files changed, 1 insertion(+), 563 deletions(-) delete mode 100644 website/client/src/assets/svg/amazonpay.svg delete mode 100644 website/client/src/components/payments/amazonModal.vue delete mode 100644 website/client/src/components/payments/buttons/amazon.vue diff --git a/website/client/src/assets/svg/amazonpay.svg b/website/client/src/assets/svg/amazonpay.svg deleted file mode 100644 index 76bd1f66c44..00000000000 --- a/website/client/src/assets/svg/amazonpay.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - -Zeichenfläche 1 - - - - - - - - - - - - - - diff --git a/website/client/src/components/news/birthdayModal.vue b/website/client/src/components/news/birthdayModal.vue index e3b2a232768..5ac89ebf6bf 100644 --- a/website/client/src/components/news/birthdayModal.vue +++ b/website/client/src/components/news/birthdayModal.vue @@ -143,12 +143,6 @@ > -
@@ -522,7 +503,7 @@ padding-bottom: 10px; } - .stripe, .paypal, .amazon { + .stripe, .paypal { width: 506px; height: 32px; margin-left: 4px; @@ -796,7 +777,6 @@ import { mapState } from '@/libs/store'; import buy from '@/mixins/buy'; import notifications from '@/mixins/notifications'; import payments from '@/mixins/payments'; -import amazonButton from '@/components/payments/buttons/amazon'; // import images import close from '@/assets/svg/new-close.svg'; @@ -805,21 +785,13 @@ import gifts from '@/assets/svg/gifts-birthday.svg'; import cross from '@/assets/svg/cross.svg'; import stripe from '@/assets/svg/stripe.svg'; import paypal from '@/assets/svg/paypal-logo.svg'; -import amazon from '@/assets/svg/amazonpay.svg'; import birthdayGems from '@/assets/svg/birthday-gems.svg'; import birthdayBackground from '@/assets/svg/icon-background-birthday.svg'; export default { - components: { - amazonButton, - }, mixins: [buy, notifications, payments], data () { return { - amazonData: { - type: 'single', - sku: 'Pet-Gryphatrice-Jubilant', - }, icons: Object.freeze({ close, confetti, @@ -827,7 +799,6 @@ export default { cross, stripe, paypal, - amazon, birthdayGems, birthdayBackground, }), diff --git a/website/client/src/components/payments/amazonModal.vue b/website/client/src/components/payments/amazonModal.vue deleted file mode 100644 index fa332be1499..00000000000 --- a/website/client/src/components/payments/amazonModal.vue +++ /dev/null @@ -1,295 +0,0 @@ - - - - - diff --git a/website/client/src/components/payments/buttons/amazon.vue b/website/client/src/components/payments/buttons/amazon.vue deleted file mode 100644 index 7c58e782140..00000000000 --- a/website/client/src/components/payments/buttons/amazon.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - - - diff --git a/website/client/src/components/payments/buyGemsModal.vue b/website/client/src/components/payments/buyGemsModal.vue index e3e1496e337..5be085e21b0 100644 --- a/website/client/src/components/payments/buyGemsModal.vue +++ b/website/client/src/components/payments/buyGemsModal.vue @@ -159,7 +159,6 @@ :paypal-fn="() => openPaypal({ url: paypalCheckoutLink, type: 'gems', gemsBlock: selectedGemsBlock })" - :amazon-data="{type: 'single', gemsBlock: selectedGemsBlock}" />
@@ -195,7 +194,6 @@ export default { subscription: { key: '' }, message: '', }, - amazonPayments: {}, assistanceEmailObject: { hrefTechAssistanceEmail: `${TECH_ASSISTANCE_EMAIL}`, }, @@ -265,9 +263,6 @@ export default { }, 500); }, onHide () { - // @TODO this breaks amazon purchases because when the amazon modal - // is opened this one is closed and the amount reset - // this.gift.gems.amount = 0; this.gift.message = ''; this.sendingInProgress = false; }, diff --git a/website/client/src/components/payments/sendGiftModal.vue b/website/client/src/components/payments/sendGiftModal.vue index 0c86b8d9e55..aa89f377643 100644 --- a/website/client/src/components/payments/sendGiftModal.vue +++ b/website/client/src/components/payments/sendGiftModal.vue @@ -153,7 +153,6 @@ :paypal-fn="() => openPaypalGift({ gift: gift, giftedTo: userReceivingGift._id, receiverName, })" - :amazon-data="{type: 'single', gift, giftedTo: userReceivingGift._id, receiverName}" />
@@ -525,7 +524,6 @@ export default { }, }, sendingInProgress: false, - amazonPayments: {}, gemCost: 1, }; }, diff --git a/website/client/src/components/settings/subscription.vue b/website/client/src/components/settings/subscription.vue index 51b61a5735a..abf9dd369b2 100644 --- a/website/client/src/components/settings/subscription.vue +++ b/website/client/src/components/settings/subscription.vue @@ -649,10 +649,6 @@ width: 448px; } - .svg-amazon-pay { - width: 208px; - } - .svg-apple-pay { width: 97.1px; height: 40px; @@ -761,7 +757,6 @@ import notificationsMixin from '../../mixins/notifications'; import subscriptionOptions from './subscriptionOptions.vue'; import Sprite from '@/components/ui/sprite'; -import amazonPayLogo from '@/assets/svg/amazonpay.svg'; import applePayLogo from '@/assets/svg/apple-pay-logo.svg'; import calendarIcon from '@/assets/svg/calendar-purple.svg'; import checkmarkIcon from '@/assets/svg/check.svg'; @@ -801,9 +796,7 @@ export default { key: null, }, // @TODO: Remove the need for this or move it to mixin - amazonPayments: {}, paymentMethods: { - AMAZON_PAYMENTS: 'Amazon Payments', STRIPE: 'Stripe', GOOGLE: 'Google', APPLE: 'Apple', @@ -811,7 +804,6 @@ export default { GIFT: 'Gift', }, icons: Object.freeze({ - amazonPayLogo, applePayLogo, calendarIcon, checkmarkIcon, @@ -923,11 +915,6 @@ export default { }, paymentMethodLogo () { switch (this.user.purchased.plan.paymentMethod) { - case this.paymentMethods.AMAZON_PAYMENTS: - return { - icon: this.icons.amazonPayLogo, - class: 'svg-amazon-pay', - }; case this.paymentMethods.APPLE: return { icon: this.icons.applePayLogo, diff --git a/website/client/src/components/static/privacy.vue b/website/client/src/components/static/privacy.vue index c2615ce69b0..50daacd0212 100644 --- a/website/client/src/components/static/privacy.vue +++ b/website/client/src/components/static/privacy.vue @@ -71,12 +71,6 @@ target="_blank" >https://stripe.com/privacy -
  • - For Amazon Pay, visit: https://pay.amazon.com/help/201751600 -
  • For PayPal, visit: - @@ -127,7 +126,6 @@ import BuyModal from '@/components/shops/buyModal.vue'; import SelectMembersModal from '@/components/selectMembersModal.vue'; import notifications from '@/mixins/notifications'; import { setup as setupPayments } from '@/libs/payments'; -import amazonPaymentsModal from '@/components/payments/amazonModal'; import paymentsSuccessModal from '@/components/payments/successModal'; import subCancelModalConfirm from '@/components/payments/cancelModalConfirm'; import subCanceledModal from '@/components/payments/canceledModal'; @@ -160,7 +158,6 @@ export default { notificationsDisplay, BuyModal, SelectMembersModal, - amazonPaymentsModal, paymentsSuccessModal, subCancelModalConfirm, subCanceledModal, From 0196e2f7ca42dfe10d4d8f08251facc72a567e55 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Thu, 21 Nov 2024 14:51:10 -0500 Subject: [PATCH 03/24] refactor(app): Revert package.json and package-lock changes --- config.json.example | 5 - website/server/controllers/api-v3/groups.js | 20 --- .../controllers/top-level/payments/amazon.js | 150 ------------------ 3 files changed, 175 deletions(-) delete mode 100644 website/server/controllers/top-level/payments/amazon.js diff --git a/config.json.example b/config.json.example index 9a50e86a222..efde45c68e7 100644 --- a/config.json.example +++ b/config.json.example @@ -1,11 +1,6 @@ { "ACCOUNT_MIN_CHAT_AGE": "0", "ADMIN_EMAIL": "you@example.com", - "AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID", - "AMAZON_PAYMENTS_MODE": "sandbox", - "AMAZON_PAYMENTS_MWS_KEY": "MWS_KEY", - "AMAZON_PAYMENTS_MWS_SECRET": "MWS_SECRET", - "AMAZON_PAYMENTS_SELLER_ID": "SELLER_ID", "AMPLITUDE_KEY": "AMPLITUDE_KEY", "AMPLITUDE_SECRET": "AMPLITUDE_SECRET", "BASE_URL": "http://localhost:3000", diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index f8dfbe86d12..655affd9429 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -25,7 +25,6 @@ import { import common from '../../../common'; import payments from '../../libs/payments/payments'; import stripePayments from '../../libs/payments/stripe'; -import amzLib from '../../libs/payments/amazon'; import { apiError } from '../../libs/apiError'; import { model as UserNotification } from '../../models/userNotification'; @@ -242,25 +241,6 @@ api.createGroupPlan = { sessionId: session.id, group: groupResponse, }); - } else if (req.body.paymentType === 'Amazon') { - const { billingAgreementId } = req.body; - const sub = req.body.subscription - ? common.content.subscriptionBlocks[req.body.subscription] - : false; - const { coupon } = req.body; - const groupId = savedGroup._id; - const { headers } = req; - - await amzLib.subscribe({ - billingAgreementId, - sub, - coupon, - user, - groupId, - headers, - }); - - res.respond(201, groupResponse); } }, }; diff --git a/website/server/controllers/top-level/payments/amazon.js b/website/server/controllers/top-level/payments/amazon.js deleted file mode 100644 index 8aa231d6849..00000000000 --- a/website/server/controllers/top-level/payments/amazon.js +++ /dev/null @@ -1,150 +0,0 @@ -import { - BadRequest, -} from '../../../libs/errors'; -import amzLib from '../../../libs/payments/amazon'; -import { - authWithHeaders, -} from '../../../middlewares/auth'; -import shared from '../../../../common'; - -const api = {}; - -/** - * @apiIgnore Payments are considered part of the private API - * @api {post} /amazon/verifyAccessToken Amazon Payments: verify access token - * @apiName AmazonVerifyAccessToken - * @apiGroup Payments - * - * @apiSuccess {Object} data Empty object - * */ -api.verifyAccessToken = { - method: 'POST', - url: '/amazon/verifyAccessToken', - middlewares: [authWithHeaders()], - async handler (req, res) { - const accessToken = req.body.access_token; - - if (!accessToken) throw new BadRequest('Missing req.body.access_token'); - - await amzLib.getTokenInfo(accessToken); - - res.respond(200, {}); - }, -}; - -/** - * @apiIgnore Payments are considered part of the private API - * @api {post} /amazon/createOrderReferenceId Amazon Payments: create order reference id - * @apiName AmazonCreateOrderReferenceId - * @apiGroup Payments - * - * @apiSuccess {String} data.orderReferenceId The order reference id. - * */ -api.createOrderReferenceId = { - method: 'POST', - url: '/amazon/createOrderReferenceId', - middlewares: [authWithHeaders()], - async handler (req, res) { - const { billingAgreementId } = req.body; - - if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId'); - - const response = await amzLib.createOrderReferenceId({ - Id: billingAgreementId, - IdType: 'BillingAgreement', - ConfirmNow: false, - }); - - res.respond(200, { - orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId, - }); - }, -}; - -/** - * @apiIgnore Payments are considered part of the private API - * @api {post} /amazon/checkout Amazon Payments: checkout - * @apiName AmazonCheckout - * @apiGroup Payments - * - * @apiSuccess {Object} data Empty object - * */ -api.checkout = { - method: 'POST', - url: '/amazon/checkout', - middlewares: [authWithHeaders()], - async handler (req, res) { - const { user } = res.locals; - const { - orderReferenceId, gift, gemsBlock, sku, - } = req.body; - - if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId'); - - await amzLib.checkout({ - gemsBlock, gift, sku, user, orderReferenceId, headers: req.headers, - }); - - res.respond(200); - }, -}; - -/** - * @apiIgnore Payments are considered part of the private API - * @api {post} /amazon/subscribe Amazon Payments: subscribe - * @apiName AmazonSubscribe - * @apiGroup Payments - * - * @apiSuccess {Object} data Empty object - * */ -api.subscribe = { - method: 'POST', - url: '/amazon/subscribe', - middlewares: [authWithHeaders()], - async handler (req, res) { - const { billingAgreementId } = req.body; - const sub = req.body.subscription - ? shared.content.subscriptionBlocks[req.body.subscription] - : false; - const { coupon } = req.body; - const { user } = res.locals; - const { groupId } = req.body; - - await amzLib.subscribe({ - billingAgreementId, - sub, - coupon, - user, - groupId, - headers: req.headers, - }); - - res.respond(200); - }, -}; - -/** - * @apiIgnore Payments are considered part of the private API - * @api {get} /amazon/subscribe/cancel Amazon Payments: subscribe cancel - * @apiName AmazonSubscribe - * @apiGroup Payments - * */ -api.subscribeCancel = { - method: 'GET', - url: '/amazon/subscribe/cancel', - middlewares: [authWithHeaders()], - async handler (req, res) { - const { user } = res.locals; - const { groupId } = req.query; - - await amzLib.cancelSubscription({ user, groupId, headers: req.headers }); - - if (req.query.noRedirect) { - res.respond(200); - } else { - res.redirect('/'); - } - }, -}; - -export default api; From e3d839288a45b082099ff279fa499a4b4b023414 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Thu, 21 Nov 2024 15:12:36 -0500 Subject: [PATCH 04/24] refactor(app): remove Amazon text strings removed Amazon strings from settings.json and limited.json --- website/common/locales/en/limited.json | 2 +- website/common/locales/en/settings.json | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/website/common/locales/en/limited.json b/website/common/locales/en/limited.json index 5935e33fe28..91534a35ac5 100644 --- a/website/common/locales/en/limited.json +++ b/website/common/locales/en/limited.json @@ -255,7 +255,7 @@ "buyNowMoneyButton": "Buy Now for $9.99", "buyNowGemsButton": "Buy Now for 60 Gems", "wantToPayWithGemsText": "Want to pay with Gems?", - "wantToPayWithMoneyText": "Want to pay with Stripe, Paypal, or Amazon?", + "wantToPayWithMoneyText": "Want to pay with a credit card or PayPal?", "ownJubilantGryphatrice": "You own the Jubilant Gryphatrice! Visit Pets and Mounts to equip!", "jubilantSuccess": "You've successfully purchased the Jubilant Gryphatrice!", "stableVisit": "Visit Pets and Mounts to equip!", diff --git a/website/common/locales/en/settings.json b/website/common/locales/en/settings.json index d5f4078eee4..9047dc19ab5 100644 --- a/website/common/locales/en/settings.json +++ b/website/common/locales/en/settings.json @@ -189,8 +189,6 @@ "nextHourglass": "Next Mystic Hourglass Delivery", "nextHourglassDescription": "Subscribers receive a Mystic Hourglass, a Mystery Gear Set, and Gems restocked in the Market within the first two days of the month", "paypal": "PayPal", - "amazonPayments": "Amazon Payments", - "amazonPaymentsRecurring": "Ticking the checkbox below is necessary for your subscription to be created. It allows your Amazon account to be used for ongoing payments for this subscription. It will not cause your Amazon account to be automatically used for any future purchases.", "timezone": "Time Zone", "timezoneUTC": "Your time zone is set by your computer, which is: <%= utc %>", "timezoneInfo": "If that time zone is wrong, first reload this page using your browser's reload or refresh button to ensure that Habitica has the most recent information. If it is still wrong, adjust the time zone on your PC and then reload this page again.

    If you use Habitica on other PCs or mobile devices, the time zone must be the same on them all. If your Dailies have been resetting at the wrong time, repeat this check on all other PCs and on a browser on your mobile devices.", From c129cfcf92118730e025a430fa9f48dc700ea23a Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Thu, 21 Nov 2024 16:10:27 -0500 Subject: [PATCH 05/24] refactor(app): update libs, mixins, and store commented out most of the code on website/client/src/store/index.js because I am unsure if this is the correct approach --- website/client/src/libs/payments.js | 32 ++++++------ website/client/src/mixins/payments.js | 73 +-------------------------- website/client/src/store/index.js | 1 - 3 files changed, 17 insertions(+), 89 deletions(-) diff --git a/website/client/src/libs/payments.js b/website/client/src/libs/payments.js index 226088ea131..7b68bf358d0 100644 --- a/website/client/src/libs/payments.js +++ b/website/client/src/libs/payments.js @@ -1,29 +1,29 @@ -import getStore from '@/store'; +// import getStore from '@/store'; export function setup () { // eslint-disable-line import/prefer-default-export - const store = getStore(); + // const store = getStore(); - // Set Amazon Payments as ready in the store, - // Added here to make sure the listener is registered before the script can be executed - window.onAmazonLoginReady = () => { - store.state.isAmazonReady = true; - window.amazon.Login.setClientId(process.env.AMAZON_PAYMENTS_CLIENT_ID); - }; + // // Set Amazon Payments as ready in the store, + // // Added here to make sure the listener is registered before the script can be executed + // window.onAmazonLoginReady = () => { + // store.state.isAmazonReady = true; + // window.amazon.Login.setClientId(process.env.AMAZON_PAYMENTS_CLIENT_ID); + // }; // Load the scripts // Amazon Payments - const amazonScript = document.createElement('script'); - let firstScript = document.getElementsByTagName('script')[0]; - amazonScript.type = 'text/javascript'; - amazonScript.async = true; - amazonScript.src = `https://static-na.payments-amazon.com/OffAmazonPayments/us/${(process.env.AMAZON_PAYMENTS_MODE === 'sandbox' ? 'sandbox/' : '')}js/Widgets.js`; - firstScript.parentNode.insertBefore(amazonScript, firstScript); + // const amazonScript = document.createElement('script'); + // let firstScript = document.getElementsByTagName('script')[0]; + // amazonScript.type = 'text/javascript'; + // amazonScript.async = true; + // amazonScript.src = `https://static-na.payments-amazon.com/OffAmazonPayments/us/${(process.env.AMAZON_PAYMENTS_MODE === 'sandbox' ? 'sandbox/' : '')}js/Widgets.js`; + // firstScript.parentNode.insertBefore(amazonScript, firstScript); // Stripe const stripeScript = document.createElement('script'); - [firstScript] = document.getElementsByTagName('script'); + // [firstScript] = document.getElementsByTagName('script'); stripeScript.async = true; stripeScript.src = 'https://js.stripe.com/v3/'; - firstScript.parentNode.insertBefore(stripeScript, firstScript); + // firstScript.parentNode.insertBefore(stripeScript, firstScript); } diff --git a/website/client/src/mixins/payments.js b/website/client/src/mixins/payments.js index 47763dccaff..d61d57155b3 100644 --- a/website/client/src/mixins/payments.js +++ b/website/client/src/mixins/payments.js @@ -263,76 +263,6 @@ export default { } return true; }, - amazonPaymentsInit (data) { - if (data.type !== 'single' && data.type !== 'subscription') return; - - if (data.type === 'single') { - this.amazonPayments.gemsBlock = data.gemsBlock; - this.amazonPayments.sku = data.sku; - } - - if (data.gift) { - if (data.gift.gems && data.gift.gems.amount && data.gift.gems.amount <= 0) return; - data.gift.uuid = data.giftedTo; - this.amazonPayments.giftReceiver = data.receiverName; - } - - if (data.subscription) { - this.amazonPayments.subscription = data.subscription; - this.amazonPayments.coupon = data.coupon; - } - - if (data.groupId) { - this.amazonPayments.groupId = data.groupId; - } - - if (data.group) { // upgrading a group - this.amazonPayments.group = data.group; - } - - if (data.groupToCreate) { // creating a group - this.amazonPayments.groupToCreate = data.groupToCreate; - } - - if (data.demographics) { // sending demographics - this.amazonPayments.demographics = data.demographics; - } - - this.amazonPayments.gift = data.gift; - this.amazonPayments.type = data.type; - }, - amazonOnError (error) { - window.alert(error.getErrorMessage()); // eslint-disable-line no-alert - this.reset(); - }, - // Make sure the amazon session is reset between different sessions and after each purchase - amazonLogout () { - if (window.amazon && window.amazon.Login && typeof window.amazon.Login.logout === 'function') { - window.amazon.Login.logout(); - } - }, - reset () { - // @TODO: Ensure we are using all of these - // some vars are set in the payments mixin. We should try to edit in one place - this.amazonLogout(); - - this.amazonPayments.modal = null; - this.amazonPayments.type = null; - this.amazonPayments.loggedIn = false; - - // Gift - this.amazonPayments.gift = null; - this.amazonPayments.giftReceiver = null; - - this.amazonPayments.billingAgreementId = null; - this.amazonPayments.orderReferenceId = null; - this.amazonPayments.paymentSelected = false; - this.amazonPayments.recurringConsent = false; - this.amazonPayments.subscription = null; - this.amazonPayments.coupon = null; - this.amazonPayments.groupToCreate = null; - this.amazonPayments.group = null; - }, cancelSubscriptionConfirm (config) { if (config.canCancel === false) return; this.$root.$emit('habitica:cancel-subscription-confirm', config); @@ -345,10 +275,9 @@ export default { group = config.group; } - let paymentMethod = group + const paymentMethod = group ? group.purchased.plan.paymentMethod : this.user.purchased.plan.paymentMethod; - paymentMethod = paymentMethod === 'Amazon Payments' ? 'amazon' : paymentMethod.toLowerCase(); const queryParams = { noRedirect: true, diff --git a/website/client/src/store/index.js b/website/client/src/store/index.js index d4db095891c..6d5d49412d7 100644 --- a/website/client/src/store/index.js +++ b/website/client/src/store/index.js @@ -56,7 +56,6 @@ export default function clientStore () { // Means the user and the user's tasks are ready // @TODO use store.user.loaded since it's an async resource? isUserLoaded: false, - isAmazonReady: false, // Whether the Amazon Payments lib can be used user: asyncResourceFactory(), // Keep track of the ids of notifications that have been removed // to make sure they don't get shown again. It happened due to concurrent requests From 2da29fed0bed8be1bd45f0062635e51cdc7a9c65 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Thu, 21 Nov 2024 16:32:04 -0500 Subject: [PATCH 06/24] refactor(app): remove Amazon code from libs, middlewares, and models --- website/server/libs/payments/amazon.js | 390 ------------------ website/server/libs/payments/paypal.js | 1 - .../server/libs/payments/stripe/constants.js | 1 - website/server/libs/payments/subscriptions.js | 4 +- website/server/middlewares/notFound.js | 1 - website/server/models/group.js | 8 +- website/server/models/user/methods.js | 1 - 7 files changed, 2 insertions(+), 404 deletions(-) delete mode 100644 website/server/libs/payments/amazon.js diff --git a/website/server/libs/payments/amazon.js b/website/server/libs/payments/amazon.js deleted file mode 100644 index 9e72340877d..00000000000 --- a/website/server/libs/payments/amazon.js +++ /dev/null @@ -1,390 +0,0 @@ -import amazonPayments from 'amazon-payments'; -import nconf from 'nconf'; -import moment from 'moment'; -import cc from 'coupon-code'; -import util from 'util'; - -import common from '../../../common'; -import { - BadRequest, - NotAuthorized, - NotFound, -} from '../errors'; -import payments from './payments'; // eslint-disable-line import/no-cycle -import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle -import { // eslint-disable-line import/no-cycle - model as Group, - basicFields as basicGroupFields, -} from '../../models/group'; -import { model as Coupon } from '../../models/coupon'; -import { getGemsBlock, validateGiftMessage } from './gems'; // eslint-disable-line import/no-cycle - -// TODO better handling of errors - -const { i18n } = common; -const IS_SANDBOX = nconf.get('AMAZON_PAYMENTS_MODE') === 'sandbox'; - -const amzPayment = amazonPayments.connect({ - environment: amazonPayments.Environment[IS_SANDBOX ? 'Sandbox' : 'Production'], - sellerId: nconf.get('AMAZON_PAYMENTS_SELLER_ID'), - mwsAccessKey: nconf.get('AMAZON_PAYMENTS_MWS_KEY'), - mwsSecretKey: nconf.get('AMAZON_PAYMENTS_MWS_SECRET'), - clientId: nconf.get('AMAZON_PAYMENTS_CLIENT_ID'), -}); - -const api = {}; - -api.constants = { - CURRENCY_CODE: 'USD', - SELLER_NOTE: 'Habitica Payment', - SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription', - SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment', - SELLER_NOTE_GROUP_NEW_MEMBER: 'Habitica Group Plan New Member', - STORE_NAME: 'Habitica', - - GIFT_TYPE_GEMS: 'gems', - GIFT_TYPE_SUBSCRIPTION: 'subscription', - - METHOD_BUY_GEMS: 'buyGems', - METHOD_BUY_SKU_ITEM: 'buySkuItem', - METHOD_CREATE_SUBSCRIPTION: 'createSubscription', - PAYMENT_METHOD: 'Amazon Payments', - PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)', -}; - -api.getTokenInfo = util.promisify(amzPayment.api.getTokenInfo).bind(amzPayment.api); -api.createOrderReferenceId = util - .promisify(amzPayment.offAmazonPayments.createOrderReferenceForId) - .bind(amzPayment.offAmazonPayments); -api.setOrderReferenceDetails = util - .promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails) - .bind(amzPayment.offAmazonPayments); -api.confirmOrderReference = util - .promisify(amzPayment.offAmazonPayments.confirmOrderReference) - .bind(amzPayment.offAmazonPayments); -api.closeOrderReference = util - .promisify(amzPayment.offAmazonPayments.closeOrderReference) - .bind(amzPayment.offAmazonPayments); -api.setBillingAgreementDetails = util - .promisify(amzPayment.offAmazonPayments.setBillingAgreementDetails) - .bind(amzPayment.offAmazonPayments); -api.getBillingAgreementDetails = util - .promisify(amzPayment.offAmazonPayments.getBillingAgreementDetails) - .bind(amzPayment.offAmazonPayments); -api.confirmBillingAgreement = util - .promisify(amzPayment.offAmazonPayments.confirmBillingAgreement) - .bind(amzPayment.offAmazonPayments); -api.closeBillingAgreement = util - .promisify(amzPayment.offAmazonPayments.closeBillingAgreement) - .bind(amzPayment.offAmazonPayments); - -api.authorizeOnBillingAgreement = function authorizeOnBillingAgreement (inputSet) { - return new Promise((resolve, reject) => { - amzPayment.offAmazonPayments.authorizeOnBillingAgreement(inputSet, (err, response) => { - if (err) return reject(err); - if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(new BadRequest(i18n.t('paymentNotSuccessful'))); - return resolve(response); - }); - }); -}; - -api.authorize = function authorize (inputSet) { - return new Promise((resolve, reject) => { - amzPayment.offAmazonPayments.authorize(inputSet, (err, response) => { - if (err) return reject(err); - if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(new BadRequest(i18n.t('paymentNotSuccessful'))); - return resolve(response); - }); - }); -}; - -/** - * Makes a purchase using Amazon Payment Lib - * - * @param options - * @param options.user The user object who is purchasing - * @param options.gift The gift details if any - * @param options.orderReferenceId The amazon orderReferenceId generated on the front end - * @param options.headers The request headers - * - * @return undefined - */ -api.checkout = async function checkout (options = {}) { - const { - gift, user, orderReferenceId, headers, gemsBlock: gemsBlockKey, sku, - } = options; - let amount; - let gemsBlock; - - if (gift) { - gift.member = await User.findById(gift.uuid).exec(); - validateGiftMessage(gift, user); - - if (gift.type === this.constants.GIFT_TYPE_GEMS) { - if (gift.gems.amount <= 0) { - throw new BadRequest(i18n.t('badAmountOfGemsToPurchase')); - } - amount = gift.gems.amount / 4; - } else if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) { - amount = common.content.subscriptionBlocks[gift.subscription.key].price; - } - } else if (sku) { - if (sku === 'Pet-Gryphatrice-Jubilant') { - amount = 9.99; - } else { - throw new NotFound('SKU not found.'); - } - } else { - gemsBlock = getGemsBlock(gemsBlockKey); - amount = gemsBlock.price / 100; - } - - if (!gift || gift.type === this.constants.GIFT_TYPE_GEMS) { - const receiver = gift ? gift.member : user; - const receiverCanGetGems = await receiver.canGetGems(); - if (!receiverCanGetGems) throw new NotAuthorized(i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); - } - - await this.setOrderReferenceDetails({ - AmazonOrderReferenceId: orderReferenceId, - OrderReferenceAttributes: { - OrderTotal: { - CurrencyCode: this.constants.CURRENCY_CODE, - Amount: amount, - }, - SellerNote: this.constants.SELLER_NOTE, - SellerOrderAttributes: { - SellerOrderId: common.uuid(), - StoreName: this.constants.STORE_NAME, - }, - }, - }); - - await this.confirmOrderReference({ AmazonOrderReferenceId: orderReferenceId }); - - await this.authorize({ - AmazonOrderReferenceId: orderReferenceId, - AuthorizationReferenceId: common.uuid().substring(0, 32), - AuthorizationAmount: { - CurrencyCode: this.constants.CURRENCY_CODE, - Amount: amount, - }, - SellerAuthorizationNote: this.constants.SELLER_NOTE, - TransactionTimeout: 0, - CaptureNow: true, - }); - - await this.closeOrderReference({ AmazonOrderReferenceId: orderReferenceId }); - - // execute payment - let method = this.constants.METHOD_BUY_GEMS; - if (sku) { - method = this.constants.METHOD_BUY_SKU_ITEM; - } - - const data = { - user, - paymentMethod: this.constants.PAYMENT_METHOD, - headers, - gemsBlock, - sku, - }; - - if (gift) { - if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) { - method = this.constants.METHOD_CREATE_SUBSCRIPTION; - } - gift.member = await User.findById(gift.uuid).exec(); - data.gift = gift; - data.paymentMethod = this.constants.PAYMENT_METHOD_GIFT; - } - - await payments[method](data); -}; - -/** - * Cancel an Amazon Subscription - * - * @param options - * @param options.user The user object who is canceling - * @param options.groupId The id of the group that is canceling - * @param options.headers The request headers - * @param options.cancellationReason A text string to control sending an email - * - * @return undefined - */ -api.cancelSubscription = async function cancelSubscription (options = {}) { - const { - user, groupId, headers, cancellationReason, - } = options; - - let billingAgreementId; - let planId; - let lastBillingDate; - - if (groupId) { - const groupFields = basicGroupFields.concat(' purchased'); - const group = await Group.getGroup({ - user, groupId, populateLeader: false, groupFields, - }); - - if (!group) { - throw new NotFound(i18n.t('groupNotFound')); - } - - if (group.leader !== user._id) { - throw new NotAuthorized(i18n.t('onlyGroupLeaderCanManageSubscription')); - } - - billingAgreementId = group.purchased.plan.customerId; - planId = group.purchased.plan.planId; - lastBillingDate = group.purchased.plan.lastBillingDate; - } else { - billingAgreementId = user.purchased.plan.customerId; - planId = user.purchased.plan.planId; - lastBillingDate = user.purchased.plan.lastBillingDate; - } - - if (!billingAgreementId) throw new NotAuthorized(i18n.t('missingSubscription')); - - const details = await this.getBillingAgreementDetails({ - AmazonBillingAgreementId: billingAgreementId, - }).catch(err => err); - - const badBAStates = ['Canceled', 'Closed', 'Suspended']; - if ( - details - && details.BillingAgreementDetails - && details.BillingAgreementDetails.BillingAgreementStatus - && badBAStates.indexOf(details.BillingAgreementDetails.BillingAgreementStatus.State) === -1 - ) { - await this.closeBillingAgreement({ - AmazonBillingAgreementId: billingAgreementId, - }); - } - - const subscriptionBlock = common.content.subscriptionBlocks[planId]; - const subscriptionLength = subscriptionBlock.months * 30; - - await payments.cancelSubscription({ - user, - groupId, - nextBill: moment(lastBillingDate).add({ days: subscriptionLength }), - paymentMethod: this.constants.PAYMENT_METHOD, - headers, - cancellationReason, - }); -}; - -/** - * Allows for purchasing a user subscription or group subscription with Amazon - * - * @param options - * @param options.billingAgreementId The Amazon billingAgreementId generated on the front end - * @param options.user The user object who is purchasing - * @param options.sub The subscription data to purchase - * @param options.coupon The coupon to discount the sub - * @param options.groupId The id of the group purchasing a subscription - * @param options.headers The request headers to store on analytics - * @return undefined - */ -api.subscribe = async function subscribe (options) { - const { - billingAgreementId, - sub, - coupon, - user, - groupId, - headers, - } = options; - - if (!sub) throw new BadRequest(i18n.t('missingSubscriptionCode')); - if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId'); - - if (sub.discount) { // apply discount - if (!coupon) throw new BadRequest(i18n.t('couponCodeRequired')); - const result = await Coupon.findOne({ _id: cc.validate(coupon), event: sub.key }).exec(); - if (!result) throw new NotAuthorized(i18n.t('invalidCoupon')); - } - - let amount = sub.price; - const leaderCount = 1; - const priceOfSingleMember = 3; - - if (groupId) { - const groupFields = basicGroupFields.concat(' purchased'); - const group = await Group.getGroup({ - user, groupId, populateLeader: false, groupFields, - }); - const membersCount = await group.getMemberCount(); - amount = sub.price + (membersCount - leaderCount) * priceOfSingleMember; - } - - await this.setBillingAgreementDetails({ - AmazonBillingAgreementId: billingAgreementId, - BillingAgreementAttributes: { - SellerNote: this.constants.SELLER_NOTE_SUBSCRIPTION, - SellerBillingAgreementAttributes: { - SellerBillingAgreementId: common.uuid(), - StoreName: this.constants.STORE_NAME, - CustomInformation: this.constants.SELLER_NOTE_SUBSCRIPTION, - }, - }, - }); - - await this.confirmBillingAgreement({ - AmazonBillingAgreementId: billingAgreementId, - }); - - await this.authorizeOnBillingAgreement({ - AmazonBillingAgreementId: billingAgreementId, - AuthorizationReferenceId: common.uuid().substring(0, 32), - AuthorizationAmount: { - CurrencyCode: this.constants.CURRENCY_CODE, - Amount: amount, - }, - SellerAuthorizationNote: this.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, - TransactionTimeout: 0, - CaptureNow: true, - SellerNote: this.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, - SellerOrderAttributes: { - SellerOrderId: common.uuid(), - StoreName: this.constants.STORE_NAME, - }, - }); - - await payments.createSubscription({ - user, - customerId: billingAgreementId, - paymentMethod: this.constants.PAYMENT_METHOD, - sub, - headers, - groupId, - }); -}; - -api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) { - // @TODO: Can we get this from the content plan? - const priceForNewMember = 3; - - // @TODO: Prorate? - - return this.authorizeOnBillingAgreement({ - AmazonBillingAgreementId: group.purchased.plan.customerId, - AuthorizationReferenceId: common.uuid().substring(0, 32), - AuthorizationAmount: { - CurrencyCode: this.constants.CURRENCY_CODE, - Amount: priceForNewMember, - }, - SellerAuthorizationNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER, - TransactionTimeout: 0, - CaptureNow: true, - SellerNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER, - SellerOrderAttributes: { - SellerOrderId: common.uuid(), - StoreName: this.constants.STORE_NAME, - }, - }); -}; - -export default api; diff --git a/website/server/libs/payments/paypal.js b/website/server/libs/payments/paypal.js index 70099e65412..f7bb9397b6f 100644 --- a/website/server/libs/payments/paypal.js +++ b/website/server/libs/payments/paypal.js @@ -60,7 +60,6 @@ api.constants = { // METHOD_BUY_GEMS: 'buyGems', // METHOD_CREATE_SUBSCRIPTION: 'createSubscription', PAYMENT_METHOD: 'Paypal', - // PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)', }; api.paypalPaymentCreate = util.promisify(paypal.payment.create.bind(paypal.payment)); diff --git a/website/server/libs/payments/stripe/constants.js b/website/server/libs/payments/stripe/constants.js index 9c5afc05b5c..45a62997523 100644 --- a/website/server/libs/payments/stripe/constants.js +++ b/website/server/libs/payments/stripe/constants.js @@ -11,5 +11,4 @@ export default { // METHOD_BUY_GEMS: 'buyGems', // METHOD_CREATE_SUBSCRIPTION: 'createSubscription', PAYMENT_METHOD: 'Stripe', - // PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)', }; diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index 4936e2b19ef..06608fd64b4 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -190,9 +190,7 @@ async function prepareSubscriptionValues (data) { paymentMethod: data.paymentMethod, extraMonths: Number(plan.extraMonths) + _dateDiff(today, plan.dateTerminated), dateTerminated: null, - // Specify a lastBillingDate just for Amazon Payments - // Resetted every time the subscription restarts - lastBillingDate: data.paymentMethod === 'Amazon Payments' ? today : undefined, + lastBillingDate: today, nextPaymentProcessing: data.nextPaymentProcessing, nextBillingDate: data.nextBillingDate, additionalData: data.additionalData, diff --git a/website/server/middlewares/notFound.js b/website/server/middlewares/notFound.js index 77f858e353b..d2c4f583669 100644 --- a/website/server/middlewares/notFound.js +++ b/website/server/middlewares/notFound.js @@ -7,7 +7,6 @@ import { serveClient } from '../libs/client'; // in which case, respond with a 404 error. const TOP_LEVEL_ROUTES = [ '/api', - '/amazon', '/iap', '/paypal', '/stripe', diff --git a/website/server/models/group.js b/website/server/models/group.js index 6db5ff3e056..db50cd90115 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -33,7 +33,6 @@ import { schema as SubscriptionPlanSchema, } from './subscriptionPlan'; import logger from '../libs/logger'; -import amazonPayments from '../libs/payments/amazon'; // eslint-disable-line import/no-cycle import stripePayments from '../libs/payments/stripe'; // eslint-disable-line import/no-cycle import { getGroupChat, translateMessage } from '../libs/chat/group-chat'; // eslint-disable-line import/no-cycle import { model as UserNotification } from './userNotification'; @@ -1622,17 +1621,12 @@ schema.methods.hasCancelled = function hasCancelled () { return Boolean(this.hasActiveGroupPlan() && plan.dateTerminated); }; -schema.methods.updateGroupPlan = async function updateGroupPlan (removingMember) { +schema.methods.updateGroupPlan = async function updateGroupPlan () { // Recheck the group plan count this.memberCount = await this.getMemberCount(); if (this.purchased.plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) { await stripePayments.chargeForAdditionalGroupMember(this); - } else if ( - this.purchased.plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD - && !removingMember - ) { - await amazonPayments.chargeForAdditionalGroupMember(this); } }; diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index e2326a73114..35ab3c28a38 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -19,7 +19,6 @@ import { model as UserNotification } from '../userNotification'; import schema from './schema'; // eslint-disable-line import/no-cycle import payments from '../../libs/payments/payments'; // eslint-disable-line import/no-cycle import * as inboxLib from '../../libs/inbox'; // eslint-disable-line import/no-cycle -import amazonPayments from '../../libs/payments/amazon'; // eslint-disable-line import/no-cycle import stripePayments from '../../libs/payments/stripe'; // eslint-disable-line import/no-cycle import paypalPayments from '../../libs/payments/paypal'; // eslint-disable-line import/no-cycle import { model as NewsPost } from '../newsPost'; From 75ba4e576de94f841b6e7abaeb430015ef507a91 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Thu, 21 Nov 2024 16:34:45 -0500 Subject: [PATCH 07/24] refactor(app): smol change to methods.js --- website/server/models/user/methods.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index 35ab3c28a38..ed97d36d232 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -343,9 +343,7 @@ schema.methods.cancelSubscription = async function cancelSubscription (options = const { plan } = this.purchased; options.user = this; - if (plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD) { - return amazonPayments.cancelSubscription(options); - } if (plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) { + if (plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) { return stripePayments.cancelSubscription(options); } if (plan.paymentMethod === paypalPayments.constants.PAYMENT_METHOD) { return paypalPayments.subscribeCancel(options); From fc2a4be8590f679fe441c4fdd3762793a3c086ea Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Fri, 22 Nov 2024 12:45:41 -0500 Subject: [PATCH 08/24] refactor(app): removed last of the backend code --- website/server/models/subscriptionPlan.js | 6 +++--- website/server/models/user/methods.js | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/website/server/models/subscriptionPlan.js b/website/server/models/subscriptionPlan.js index 4943705e898..067d4063abf 100644 --- a/website/server/models/subscriptionPlan.js +++ b/website/server/models/subscriptionPlan.js @@ -8,8 +8,8 @@ export const schema = new mongoose.Schema({ subscriptionId: String, owner: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for subscription owner.'] }, quantity: { $type: Number, default: 1 }, - paymentMethod: String, // enum: ['Paypal', 'Stripe', 'Gift', 'Amazon Payments', 'Google', '']} - customerId: String, // Billing Agreement Id in case of Amazon Payments + paymentMethod: String, // enum: ['Paypal', 'Stripe', 'Gift', 'Google', '']} + customerId: String, dateCreated: Date, dateTerminated: Date, dateUpdated: Date, @@ -18,7 +18,7 @@ export const schema = new mongoose.Schema({ gemsBought: { $type: Number, default: 0 }, mysteryItems: { $type: Array, default: () => [] }, lastReminderDate: Date, // indicates the last time a subscription reminder was sent - lastBillingDate: Date, // Used only for Amazon Payments to keep track of billing date + lastBillingDate: Date, // Example for Google: {'receipt': 'serialized receipt json', 'signature': 'signature string'} additionalData: mongoose.Schema.Types.Mixed, // indicates when the queue server should process this subscription again. diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index ed97d36d232..847292afd0e 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -325,7 +325,6 @@ schema.statics.addComputedStatsToJSONObj = function addComputedStatsToUserJSONOb * @param options * @param options.user The user object who is purchasing * @param options.groupId The id of the group purchasing a subscription - * @param options.headers The request headers (only for Amazon subscriptions) * @param options.cancellationReason A text string to control sending an email * * @return a Promise from api.cancelSubscription() From 40569111389c735a7c1d077b906788e98379b0b2 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Wed, 27 Nov 2024 11:26:58 -0500 Subject: [PATCH 09/24] refactor(app): update mixins/payments.js --- website/client/src/mixins/payments.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/client/src/mixins/payments.js b/website/client/src/mixins/payments.js index d61d57155b3..0dd23830229 100644 --- a/website/client/src/mixins/payments.js +++ b/website/client/src/mixins/payments.js @@ -275,7 +275,8 @@ export default { group = config.group; } - const paymentMethod = group + // eslint-disable-next-line + let paymentMethod = group ? group.purchased.plan.paymentMethod : this.user.purchased.plan.paymentMethod; From 74b9e83bf8345b72a4a46d724a25da97b9d66021 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Wed, 27 Nov 2024 11:50:21 -0500 Subject: [PATCH 10/24] refactor(app): revert mixins/payments.js to develop --- website/client/src/mixins/payments.js | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/website/client/src/mixins/payments.js b/website/client/src/mixins/payments.js index 0dd23830229..fe38f24341d 100644 --- a/website/client/src/mixins/payments.js +++ b/website/client/src/mixins/payments.js @@ -263,6 +263,76 @@ export default { } return true; }, + amazonPaymentsInit (data) { + if (data.type !== 'single' && data.type !== 'subscription') return; + + if (data.type === 'single') { + this.amazonPayments.gemsBlock = data.gemsBlock; + this.amazonPayments.sku = data.sku; + } + + if (data.gift) { + if (data.gift.gems && data.gift.gems.amount && data.gift.gems.amount <= 0) return; + data.gift.uuid = data.giftedTo; + this.amazonPayments.giftReceiver = data.receiverName; + } + + if (data.subscription) { + this.amazonPayments.subscription = data.subscription; + this.amazonPayments.coupon = data.coupon; + } + + if (data.groupId) { + this.amazonPayments.groupId = data.groupId; + } + + if (data.group) { // upgrading a group + this.amazonPayments.group = data.group; + } + + if (data.groupToCreate) { // creating a group + this.amazonPayments.groupToCreate = data.groupToCreate; + } + + if (data.demographics) { // sending demographics + this.amazonPayments.demographics = data.demographics; + } + + this.amazonPayments.gift = data.gift; + this.amazonPayments.type = data.type; + }, + amazonOnError (error) { + window.alert(error.getErrorMessage()); // eslint-disable-line no-alert + this.reset(); + }, + // Make sure the amazon session is reset between different sessions and after each purchase + amazonLogout () { + if (window.amazon && window.amazon.Login && typeof window.amazon.Login.logout === 'function') { + window.amazon.Login.logout(); + } + }, + reset () { + // @TODO: Ensure we are using all of these + // some vars are set in the payments mixin. We should try to edit in one place + this.amazonLogout(); + + this.amazonPayments.modal = null; + this.amazonPayments.type = null; + this.amazonPayments.loggedIn = false; + + // Gift + this.amazonPayments.gift = null; + this.amazonPayments.giftReceiver = null; + + this.amazonPayments.billingAgreementId = null; + this.amazonPayments.orderReferenceId = null; + this.amazonPayments.paymentSelected = false; + this.amazonPayments.recurringConsent = false; + this.amazonPayments.subscription = null; + this.amazonPayments.coupon = null; + this.amazonPayments.groupToCreate = null; + this.amazonPayments.group = null; + }, cancelSubscriptionConfirm (config) { if (config.canCancel === false) return; this.$root.$emit('habitica:cancel-subscription-confirm', config); @@ -279,6 +349,7 @@ export default { let paymentMethod = group ? group.purchased.plan.paymentMethod : this.user.purchased.plan.paymentMethod; + paymentMethod = paymentMethod === 'Amazon Payments' ? 'amazon' : paymentMethod.toLowerCase(); const queryParams = { noRedirect: true, From 8960ad06dbc8c8497f683f3c7573ab0e04817c3e Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Wed, 27 Nov 2024 12:05:37 -0500 Subject: [PATCH 11/24] refactor(app): remove leading spaces --- website/client/src/mixins/payments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/client/src/mixins/payments.js b/website/client/src/mixins/payments.js index fe38f24341d..20634fdde39 100644 --- a/website/client/src/mixins/payments.js +++ b/website/client/src/mixins/payments.js @@ -349,7 +349,7 @@ export default { let paymentMethod = group ? group.purchased.plan.paymentMethod : this.user.purchased.plan.paymentMethod; - paymentMethod = paymentMethod === 'Amazon Payments' ? 'amazon' : paymentMethod.toLowerCase(); + paymentMethod = paymentMethod === 'Amazon Payments' ? 'amazon' : paymentMethod.toLowerCase(); const queryParams = { noRedirect: true, From bfd0f19e997d6098a2c1d4e48a74025263d30828 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Wed, 27 Nov 2024 12:26:52 -0500 Subject: [PATCH 12/24] refactor(app): revert mixins/payments.js to develop --- website/client/src/mixins/payments.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/client/src/mixins/payments.js b/website/client/src/mixins/payments.js index 20634fdde39..831438714a9 100644 --- a/website/client/src/mixins/payments.js +++ b/website/client/src/mixins/payments.js @@ -345,7 +345,6 @@ export default { group = config.group; } - // eslint-disable-next-line let paymentMethod = group ? group.purchased.plan.paymentMethod : this.user.purchased.plan.paymentMethod; @@ -398,4 +397,4 @@ export default { this.redirectToStripe(paymentData); }, }, -}; +}; \ No newline at end of file From 503fd93e2b8590410a3910d3994aec8a0505a0a1 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Wed, 27 Nov 2024 12:52:45 -0500 Subject: [PATCH 13/24] refactor(app): add new line at end of payments.js --- website/client/src/mixins/payments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/client/src/mixins/payments.js b/website/client/src/mixins/payments.js index 831438714a9..47763dccaff 100644 --- a/website/client/src/mixins/payments.js +++ b/website/client/src/mixins/payments.js @@ -397,4 +397,4 @@ export default { this.redirectToStripe(paymentData); }, }, -}; \ No newline at end of file +}; From 7d0ce8a0846339129000ec2879a80aa345ede1d7 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Wed, 27 Nov 2024 13:50:30 -0500 Subject: [PATCH 14/24] refactor(app): restored code to libs/payments.js --- website/client/src/libs/payments.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/website/client/src/libs/payments.js b/website/client/src/libs/payments.js index 7b68bf358d0..2c568e218cc 100644 --- a/website/client/src/libs/payments.js +++ b/website/client/src/libs/payments.js @@ -1,29 +1,29 @@ -// import getStore from '@/store'; +import getStore from '@/store'; export function setup () { // eslint-disable-line import/prefer-default-export - // const store = getStore(); + const store = getStore(); // // Set Amazon Payments as ready in the store, // // Added here to make sure the listener is registered before the script can be executed - // window.onAmazonLoginReady = () => { - // store.state.isAmazonReady = true; - // window.amazon.Login.setClientId(process.env.AMAZON_PAYMENTS_CLIENT_ID); - // }; + window.onAmazonLoginReady = () => { + store.state.isAmazonReady = true; + window.amazon.Login.setClientId(process.env.AMAZON_PAYMENTS_CLIENT_ID); + }; // Load the scripts // Amazon Payments - // const amazonScript = document.createElement('script'); - // let firstScript = document.getElementsByTagName('script')[0]; - // amazonScript.type = 'text/javascript'; - // amazonScript.async = true; - // amazonScript.src = `https://static-na.payments-amazon.com/OffAmazonPayments/us/${(process.env.AMAZON_PAYMENTS_MODE === 'sandbox' ? 'sandbox/' : '')}js/Widgets.js`; - // firstScript.parentNode.insertBefore(amazonScript, firstScript); + const amazonScript = document.createElement('script'); + let firstScript = document.getElementsByTagName('script')[0]; + amazonScript.type = 'text/javascript'; + amazonScript.async = true; + amazonScript.src = `https://static-na.payments-amazon.com/OffAmazonPayments/us/${(process.env.AMAZON_PAYMENTS_MODE === 'sandbox' ? 'sandbox/' : '')}js/Widgets.js`; + firstScript.parentNode.insertBefore(amazonScript, firstScript); // Stripe const stripeScript = document.createElement('script'); - // [firstScript] = document.getElementsByTagName('script'); + [firstScript] = document.getElementsByTagName('script'); stripeScript.async = true; stripeScript.src = 'https://js.stripe.com/v3/'; - // firstScript.parentNode.insertBefore(stripeScript, firstScript); + firstScript.parentNode.insertBefore(stripeScript, firstScript); } From 93a3693dd482849e9c8b8c64289297155404d5e6 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Mon, 2 Dec 2024 14:39:33 -0500 Subject: [PATCH 15/24] refactor(app): remove amz code from src/mixins and src/libs files affected: src/mixins/payments.js and src/libs/payments.json --- website/client/src/libs/payments.js | 19 +------ website/client/src/mixins/payments.js | 71 +-------------------------- 2 files changed, 3 insertions(+), 87 deletions(-) diff --git a/website/client/src/libs/payments.js b/website/client/src/libs/payments.js index 2c568e218cc..d41efbc4a35 100644 --- a/website/client/src/libs/payments.js +++ b/website/client/src/libs/payments.js @@ -3,26 +3,11 @@ import getStore from '@/store'; export function setup () { // eslint-disable-line import/prefer-default-export const store = getStore(); - // // Set Amazon Payments as ready in the store, - // // Added here to make sure the listener is registered before the script can be executed - window.onAmazonLoginReady = () => { - store.state.isAmazonReady = true; - window.amazon.Login.setClientId(process.env.AMAZON_PAYMENTS_CLIENT_ID); - }; - - // Load the scripts - - // Amazon Payments - const amazonScript = document.createElement('script'); - let firstScript = document.getElementsByTagName('script')[0]; - amazonScript.type = 'text/javascript'; - amazonScript.async = true; - amazonScript.src = `https://static-na.payments-amazon.com/OffAmazonPayments/us/${(process.env.AMAZON_PAYMENTS_MODE === 'sandbox' ? 'sandbox/' : '')}js/Widgets.js`; - firstScript.parentNode.insertBefore(amazonScript, firstScript); + // Load the payment scripts // Stripe const stripeScript = document.createElement('script'); - [firstScript] = document.getElementsByTagName('script'); + let firstScript = document.getElementsByTagName('script')[0]; stripeScript.async = true; stripeScript.src = 'https://js.stripe.com/v3/'; firstScript.parentNode.insertBefore(stripeScript, firstScript); diff --git a/website/client/src/mixins/payments.js b/website/client/src/mixins/payments.js index 47763dccaff..b8d724f313e 100644 --- a/website/client/src/mixins/payments.js +++ b/website/client/src/mixins/payments.js @@ -263,76 +263,7 @@ export default { } return true; }, - amazonPaymentsInit (data) { - if (data.type !== 'single' && data.type !== 'subscription') return; - if (data.type === 'single') { - this.amazonPayments.gemsBlock = data.gemsBlock; - this.amazonPayments.sku = data.sku; - } - - if (data.gift) { - if (data.gift.gems && data.gift.gems.amount && data.gift.gems.amount <= 0) return; - data.gift.uuid = data.giftedTo; - this.amazonPayments.giftReceiver = data.receiverName; - } - - if (data.subscription) { - this.amazonPayments.subscription = data.subscription; - this.amazonPayments.coupon = data.coupon; - } - - if (data.groupId) { - this.amazonPayments.groupId = data.groupId; - } - - if (data.group) { // upgrading a group - this.amazonPayments.group = data.group; - } - - if (data.groupToCreate) { // creating a group - this.amazonPayments.groupToCreate = data.groupToCreate; - } - - if (data.demographics) { // sending demographics - this.amazonPayments.demographics = data.demographics; - } - - this.amazonPayments.gift = data.gift; - this.amazonPayments.type = data.type; - }, - amazonOnError (error) { - window.alert(error.getErrorMessage()); // eslint-disable-line no-alert - this.reset(); - }, - // Make sure the amazon session is reset between different sessions and after each purchase - amazonLogout () { - if (window.amazon && window.amazon.Login && typeof window.amazon.Login.logout === 'function') { - window.amazon.Login.logout(); - } - }, - reset () { - // @TODO: Ensure we are using all of these - // some vars are set in the payments mixin. We should try to edit in one place - this.amazonLogout(); - - this.amazonPayments.modal = null; - this.amazonPayments.type = null; - this.amazonPayments.loggedIn = false; - - // Gift - this.amazonPayments.gift = null; - this.amazonPayments.giftReceiver = null; - - this.amazonPayments.billingAgreementId = null; - this.amazonPayments.orderReferenceId = null; - this.amazonPayments.paymentSelected = false; - this.amazonPayments.recurringConsent = false; - this.amazonPayments.subscription = null; - this.amazonPayments.coupon = null; - this.amazonPayments.groupToCreate = null; - this.amazonPayments.group = null; - }, cancelSubscriptionConfirm (config) { if (config.canCancel === false) return; this.$root.$emit('habitica:cancel-subscription-confirm', config); @@ -348,7 +279,7 @@ export default { let paymentMethod = group ? group.purchased.plan.paymentMethod : this.user.purchased.plan.paymentMethod; - paymentMethod = paymentMethod === 'Amazon Payments' ? 'amazon' : paymentMethod.toLowerCase(); + paymentMethod = paymentMethod.toLowerCase(); const queryParams = { noRedirect: true, From 51470f7358933e7ab9cea23bf40bced7af1917f6 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Mon, 2 Dec 2024 14:47:30 -0500 Subject: [PATCH 16/24] chore(app): fix deviance of header from develop --- website/client/src/components/header/menu.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/client/src/components/header/menu.vue b/website/client/src/components/header/menu.vue index f1b04ac4898..3292277d4cd 100644 --- a/website/client/src/components/header/menu.vue +++ b/website/client/src/components/header/menu.vue @@ -354,13 +354,15 @@ >
  • {{ userHourglasses }} -
    +
    {{ userGems }} From 8d210485e6ef6802713f3c8887c972fee70d87a4 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Mon, 2 Dec 2024 15:14:21 -0500 Subject: [PATCH 17/24] fix(shared): fix to src/libs/payments commented out import and const store; change let to const on line 10 --- website/client/src/libs/payments.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/client/src/libs/payments.js b/website/client/src/libs/payments.js index d41efbc4a35..f3c2a840ddf 100644 --- a/website/client/src/libs/payments.js +++ b/website/client/src/libs/payments.js @@ -1,13 +1,13 @@ -import getStore from '@/store'; +// import getStore from '@/store'; export function setup () { // eslint-disable-line import/prefer-default-export - const store = getStore(); + // const store = getStore(); // Load the payment scripts // Stripe const stripeScript = document.createElement('script'); - let firstScript = document.getElementsByTagName('script')[0]; + const firstScript = document.getElementsByTagName('script')[0]; stripeScript.async = true; stripeScript.src = 'https://js.stripe.com/v3/'; firstScript.parentNode.insertBefore(stripeScript, firstScript); From d6c8b0e7e346932b9812e0e025462572998f6dd5 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Tue, 3 Dec 2024 13:49:10 -0500 Subject: [PATCH 18/24] refactor(app): remove code from tests, api, and vue.config.js --- .../unit/libs/payments/amazon/cancel.test.js | 180 ------------ .../libs/payments/amazon/checkout.test.js | 230 --------------- .../libs/payments/amazon/subscribe.test.js | 276 ------------------ .../payments/amazon/upgrade-groupplan.test.js | 86 ------ .../group-plans/group-payments-create.test.js | 74 ----- test/api/unit/libs/payments/payments.test.js | 8 - ...T-payments_amazon_subscribe_cancel.test.js | 78 ----- .../POST-payments_amazon_checkout.test.js | 64 ---- ...ents_amazon_createOrderReferenceId.test.js | 20 -- .../POST-payments_amazon_subscribe.test.js | 97 ------ ...-payments_amazon_verifyAccessToken.test.js | 20 -- test/helpers/api-integration/requester.js | 1 - website/client/vue.config.js | 7 - 13 files changed, 1141 deletions(-) delete mode 100644 test/api/unit/libs/payments/amazon/cancel.test.js delete mode 100644 test/api/unit/libs/payments/amazon/checkout.test.js delete mode 100644 test/api/unit/libs/payments/amazon/subscribe.test.js delete mode 100644 test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js delete mode 100644 test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js delete mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js delete mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js delete mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js delete mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js diff --git a/test/api/unit/libs/payments/amazon/cancel.test.js b/test/api/unit/libs/payments/amazon/cancel.test.js deleted file mode 100644 index e24d3dbf522..00000000000 --- a/test/api/unit/libs/payments/amazon/cancel.test.js +++ /dev/null @@ -1,180 +0,0 @@ -import moment from 'moment'; - -import { - generateGroup, -} from '../../../../../helpers/api-unit.helper'; -import { model as User } from '../../../../../../website/server/models/user'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; -import payments from '../../../../../../website/server/libs/payments/payments'; -import common from '../../../../../../website/common'; -import { createNonLeaderGroupMember } from '../paymentHelpers'; - -const { i18n } = common; - -describe('Amazon Payments - Cancel Subscription', () => { - const subKey = 'basic_3mo'; - - let user; let group; let headers; let billingAgreementId; let subscriptionBlock; let - subscriptionLength; - let getBillingAgreementDetailsSpy; - let paymentCancelSubscriptionSpy; - - function expectAmazonStubs () { - expect(getBillingAgreementDetailsSpy).to.be.calledOnce; - expect(getBillingAgreementDetailsSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - }); - } - - function expectAmazonCancelSubscriptionSpy (groupId, lastBillingDate) { - expect(paymentCancelSubscriptionSpy).to.be.calledWith({ - user, - groupId, - nextBill: moment(lastBillingDate).add({ days: subscriptionLength }), - paymentMethod: amzLib.constants.PAYMENT_METHOD, - headers, - cancellationReason: undefined, - }); - } - - function expectAmazonCancelUserSubscriptionSpy () { - expect(paymentCancelSubscriptionSpy).to.be.calledOnce; - expectAmazonCancelSubscriptionSpy(undefined, user.purchased.plan.lastBillingDate); - } - - function expectAmazonCancelGroupSubscriptionSpy (groupId) { - expect(paymentCancelSubscriptionSpy).to.be.calledOnce; - expectAmazonCancelSubscriptionSpy(groupId, group.purchased.plan.lastBillingDate); - } - - function expectBillingAggreementDetailSpy () { - getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails') - .resolves({ - BillingAgreementDetails: { - BillingAgreementStatus: { State: 'Open' }, - }, - }); - } - - beforeEach(async () => { - user = new User(); - user.profile.name = 'sender'; - user.purchased.plan.customerId = 'customer-id'; - user.purchased.plan.planId = subKey; - user.purchased.plan.lastBillingDate = new Date(); - - group = generateGroup({ - name: 'test group', - type: 'guild', - privacy: 'public', - leader: user._id, - }); - group.purchased.plan.customerId = 'customer-id'; - group.purchased.plan.planId = subKey; - group.purchased.plan.lastBillingDate = new Date(); - await group.save(); - - subscriptionBlock = common.content.subscriptionBlocks[subKey]; - subscriptionLength = subscriptionBlock.months * 30; - - headers = {}; - - getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails'); - getBillingAgreementDetailsSpy.resolves({ - BillingAgreementDetails: { - BillingAgreementStatus: { State: 'Closed' }, - }, - }); - - paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription'); - paymentCancelSubscriptionSpy.resolves({}); - }); - - afterEach(() => { - amzLib.getBillingAgreementDetails.restore(); - payments.cancelSubscription.restore(); - }); - - it('should throw an error if we are missing a subscription', async () => { - user.purchased.plan.customerId = undefined; - - await expect(amzLib.cancelSubscription({ user })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 401, - name: 'NotAuthorized', - message: i18n.t('missingSubscription'), - }); - }); - - it('should cancel a user subscription', async () => { - billingAgreementId = user.purchased.plan.customerId; - - await amzLib.cancelSubscription({ user, headers }); - - expectAmazonCancelUserSubscriptionSpy(); - expectAmazonStubs(); - }); - - it('should close a user subscription if amazon not closed', async () => { - amzLib.getBillingAgreementDetails.restore(); - expectBillingAggreementDetailSpy(); - const closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); - billingAgreementId = user.purchased.plan.customerId; - - await amzLib.cancelSubscription({ user, headers }); - - expectAmazonStubs(); - expect(closeBillingAgreementSpy).to.be.calledOnce; - expect(closeBillingAgreementSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - }); - expectAmazonCancelUserSubscriptionSpy(); - amzLib.closeBillingAgreement.restore(); - }); - - it('should throw an error if group is not found', async () => { - await expect(amzLib.cancelSubscription({ user, groupId: 'fake-id' })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 404, - name: 'NotFound', - message: i18n.t('groupNotFound'), - }); - }); - - it('should throw an error if user is not group leader', async () => { - const nonLeader = await createNonLeaderGroupMember(group); - - await expect(amzLib.cancelSubscription({ user: nonLeader, groupId: group._id })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 401, - name: 'NotAuthorized', - message: i18n.t('onlyGroupLeaderCanManageSubscription'), - }); - }); - - it('should cancel a group subscription', async () => { - billingAgreementId = group.purchased.plan.customerId; - - await amzLib.cancelSubscription({ user, groupId: group._id, headers }); - - expectAmazonCancelGroupSubscriptionSpy(group._id); - expectAmazonStubs(); - }); - - it('should close a group subscription if amazon not closed', async () => { - amzLib.getBillingAgreementDetails.restore(); - expectBillingAggreementDetailSpy(); - const closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); - billingAgreementId = group.purchased.plan.customerId; - - await amzLib.cancelSubscription({ user, groupId: group._id, headers }); - - expectAmazonStubs(); - expect(closeBillingAgreementSpy).to.be.calledOnce; - expect(closeBillingAgreementSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - }); - expectAmazonCancelGroupSubscriptionSpy(group._id); - amzLib.closeBillingAgreement.restore(); - }); -}); diff --git a/test/api/unit/libs/payments/amazon/checkout.test.js b/test/api/unit/libs/payments/amazon/checkout.test.js deleted file mode 100644 index c11a3932f32..00000000000 --- a/test/api/unit/libs/payments/amazon/checkout.test.js +++ /dev/null @@ -1,230 +0,0 @@ -import { model as User } from '../../../../../../website/server/models/user'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; -import payments from '../../../../../../website/server/libs/payments/payments'; -import common from '../../../../../../website/common'; -import { apiError } from '../../../../../../website/server/libs/apiError'; -import * as gems from '../../../../../../website/server/libs/payments/gems'; - -const { i18n } = common; - -describe('Amazon Payments - Checkout', () => { - const subKey = 'basic_3mo'; - let user; let orderReferenceId; let - headers; const gemsBlockKey = '21gems'; const gemsBlock = common.content.gems[gemsBlockKey]; - let setOrderReferenceDetailsSpy; - let confirmOrderReferenceSpy; - let authorizeSpy; - let closeOrderReferenceSpy; - - let paymentBuyGemsStub; - let paymentCreateSubscriptionStub; - let amount = gemsBlock.price / 100; - - function expectOrderReferenceSpy () { - expect(setOrderReferenceDetailsSpy).to.be.calledOnce; - expect(setOrderReferenceDetailsSpy).to.be.calledWith({ - AmazonOrderReferenceId: orderReferenceId, - OrderReferenceAttributes: { - OrderTotal: { - CurrencyCode: amzLib.constants.CURRENCY_CODE, - Amount: amount, - }, - SellerNote: amzLib.constants.SELLER_NOTE, - SellerOrderAttributes: { - SellerOrderId: common.uuid(), - StoreName: amzLib.constants.STORE_NAME, - }, - }, - }); - } - - function expectAuthorizeSpy () { - expect(authorizeSpy).to.be.calledOnce; - expect(authorizeSpy).to.be.calledWith({ - AmazonOrderReferenceId: orderReferenceId, - AuthorizationReferenceId: common.uuid().substring(0, 32), - AuthorizationAmount: { - CurrencyCode: amzLib.constants.CURRENCY_CODE, - Amount: amount, - }, - SellerAuthorizationNote: amzLib.constants.SELLER_NOTE, - TransactionTimeout: 0, - CaptureNow: true, - }); - } - - function expectAmazonStubs () { - expectOrderReferenceSpy(); - - expect(confirmOrderReferenceSpy).to.be.calledOnce; - expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId }); - - expectAuthorizeSpy(); - - expect(closeOrderReferenceSpy).to.be.calledOnce; - expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId }); - } - - beforeEach(() => { - user = new User(); - headers = {}; - orderReferenceId = 'orderReferenceId'; - - setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails'); - setOrderReferenceDetailsSpy.resolves({}); - - confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference'); - confirmOrderReferenceSpy.resolves({}); - - authorizeSpy = sinon.stub(amzLib, 'authorize'); - authorizeSpy.resolves({}); - - closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference'); - closeOrderReferenceSpy.resolves({}); - - paymentBuyGemsStub = sinon.stub(payments, 'buyGems'); - paymentBuyGemsStub.resolves({}); - - paymentCreateSubscriptionStub = sinon.stub(payments, 'createSubscription'); - paymentCreateSubscriptionStub.resolves({}); - - sinon.stub(common, 'uuid').returns('uuid-generated'); - sandbox.stub(gems, 'validateGiftMessage'); - }); - - afterEach(() => { - amzLib.setOrderReferenceDetails.restore(); - amzLib.confirmOrderReference.restore(); - amzLib.authorize.restore(); - amzLib.closeOrderReference.restore(); - payments.buyGems.restore(); - payments.createSubscription.restore(); - common.uuid.restore(); - }); - - function expectBuyGemsStub (paymentMethod, gift) { - expect(paymentBuyGemsStub).to.be.calledOnce; - - const expectedArgs = { - user, - paymentMethod, - headers, - sku: undefined, - }; - if (gift) { - expectedArgs.gift = gift; - expectedArgs.gemsBlock = undefined; - expect(gems.validateGiftMessage).to.be.calledOnce; - expect(gems.validateGiftMessage).to.be.calledWith(gift, user); - } else { - expect(gems.validateGiftMessage).to.not.be.called; - expectedArgs.gemsBlock = gemsBlock; - } - expect(paymentBuyGemsStub).to.be.calledWith(expectedArgs); - } - - it('should purchase gems', async () => { - sinon.stub(user, 'canGetGems').resolves(true); - await amzLib.checkout({ - user, orderReferenceId, headers, gemsBlock: gemsBlockKey, - }); - - expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD); - expectAmazonStubs(); - expect(user.canGetGems).to.be.calledOnce; - user.canGetGems.restore(); - }); - - it('should error if gem amount is too low', async () => { - const receivingUser = new User(); - receivingUser.save(); - const gift = { - type: 'gems', - gems: { - amount: 0, - uuid: receivingUser._id, - }, - }; - - await expect(amzLib.checkout({ - gift, user, orderReferenceId, headers, - })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 400, - message: 'Amount must be at least 1.', - name: 'BadRequest', - }); - }); - - it('should error if user cannot get gems gems', async () => { - sinon.stub(user, 'canGetGems').resolves(false); - await expect(amzLib.checkout({ - user, orderReferenceId, headers, gemsBlock: gemsBlockKey, - })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 401, - message: i18n.t('groupPolicyCannotGetGems'), - name: 'NotAuthorized', - }); - user.canGetGems.restore(); - }); - - it('should error if gems block is not valid', async () => { - await expect(amzLib.checkout({ - user, orderReferenceId, headers, gemsBlock: 'invalid', - })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 400, - message: apiError('invalidGemsBlock'), - name: 'BadRequest', - }); - }); - - it('should gift gems', async () => { - const receivingUser = new User(); - await receivingUser.save(); - const gift = { - type: 'gems', - uuid: receivingUser._id, - gems: { - amount: 16, - }, - }; - amount = 16 / 4; - await amzLib.checkout({ - gift, user, orderReferenceId, headers, - }); - - expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD_GIFT, gift); - expectAmazonStubs(); - }); - - it('should gift a subscription', async () => { - const receivingUser = new User(); - receivingUser.save(); - const gift = { - type: 'subscription', - subscription: { - key: subKey, - uuid: receivingUser._id, - }, - }; - amount = common.content.subscriptionBlocks[subKey].price; - - await amzLib.checkout({ - user, orderReferenceId, headers, gift, - }); - - gift.member = receivingUser; - expect(paymentCreateSubscriptionStub).to.be.calledOnce; - expect(paymentCreateSubscriptionStub).to.be.calledWith({ - user, - paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT, - headers, - gift, - gemsBlock: undefined, - sku: undefined, - }); - expectAmazonStubs(); - }); -}); diff --git a/test/api/unit/libs/payments/amazon/subscribe.test.js b/test/api/unit/libs/payments/amazon/subscribe.test.js deleted file mode 100644 index 03cef6b0bcf..00000000000 --- a/test/api/unit/libs/payments/amazon/subscribe.test.js +++ /dev/null @@ -1,276 +0,0 @@ -import cc from 'coupon-code'; - -import { - generateGroup, -} from '../../../../../helpers/api-unit.helper'; -import { model as User } from '../../../../../../website/server/models/user'; -import { model as Coupon } from '../../../../../../website/server/models/coupon'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; -import payments from '../../../../../../website/server/libs/payments/payments'; -import common from '../../../../../../website/common'; - -const { i18n } = common; - -describe('Amazon Payments - Subscribe', () => { - const subKey = 'basic_3mo'; - let user; let group; let amount; let billingAgreementId; let sub; let coupon; let groupId; let - headers; - let amazonSetBillingAgreementDetailsSpy; - let amazonConfirmBillingAgreementSpy; - let amazonAuthorizeOnBillingAgreementSpy; - let createSubSpy; - - beforeEach(async () => { - user = new User(); - user.profile.name = 'sender'; - user.purchased.plan.customerId = 'customer-id'; - user.purchased.plan.planId = subKey; - user.purchased.plan.lastBillingDate = new Date(); - - group = generateGroup({ - name: 'test group', - type: 'guild', - privacy: 'public', - leader: user._id, - }); - group.purchased.plan.customerId = 'customer-id'; - group.purchased.plan.planId = subKey; - await group.save(); - - amount = common.content.subscriptionBlocks[subKey].price; - billingAgreementId = 'billingAgreementId'; - sub = { - key: subKey, - price: amount, - }; - groupId = group._id; - headers = {}; - - amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails'); - amazonSetBillingAgreementDetailsSpy.resolves({}); - - amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement'); - amazonConfirmBillingAgreementSpy.resolves({}); - - amazonAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement'); - amazonAuthorizeOnBillingAgreementSpy.resolves({}); - - createSubSpy = sinon.stub(payments, 'createSubscription'); - createSubSpy.resolves({}); - - sinon.stub(common, 'uuid').returns('uuid-generated'); - }); - - afterEach(() => { - amzLib.setBillingAgreementDetails.restore(); - amzLib.confirmBillingAgreement.restore(); - amzLib.authorizeOnBillingAgreement.restore(); - payments.createSubscription.restore(); - common.uuid.restore(); - }); - - function expectAmazonAuthorizeBillingAgreementSpy () { - expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledOnce; - expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - AuthorizationReferenceId: common.uuid().substring(0, 32), - AuthorizationAmount: { - CurrencyCode: amzLib.constants.CURRENCY_CODE, - Amount: amount, - }, - SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, - TransactionTimeout: 0, - CaptureNow: true, - SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, - SellerOrderAttributes: { - SellerOrderId: common.uuid(), - StoreName: amzLib.constants.STORE_NAME, - }, - }); - } - - function expectAmazonSetBillingAgreementDetailsSpy () { - expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce; - expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - BillingAgreementAttributes: { - SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION, - SellerBillingAgreementAttributes: { - SellerBillingAgreementId: common.uuid(), - StoreName: amzLib.constants.STORE_NAME, - CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION, - }, - }, - }); - } - - function expectCreateSpy () { - expect(createSubSpy).to.be.calledOnce; - expect(createSubSpy).to.be.calledWith({ - user, - customerId: billingAgreementId, - paymentMethod: amzLib.constants.PAYMENT_METHOD, - sub, - headers, - groupId, - }); - } - - it('should throw an error if we are missing a subscription', async () => { - await expect(amzLib.subscribe({ - billingAgreementId, - coupon, - user, - groupId, - headers, - })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 400, - name: 'BadRequest', - message: i18n.t('missingSubscriptionCode'), - }); - }); - - it('should throw an error if we are missing a billingAgreementId', async () => { - await expect(amzLib.subscribe({ - sub, - coupon, - user, - groupId, - headers, - })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 400, - name: 'BadRequest', - message: 'Missing req.body.billingAgreementId', - }); - }); - - it('should throw an error when coupon code is missing', async () => { - sub.discount = 40; - - await expect(amzLib.subscribe({ - billingAgreementId, - sub, - coupon, - user, - groupId, - headers, - })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 400, - name: 'BadRequest', - message: i18n.t('couponCodeRequired'), - }); - }); - - it('should throw an error when coupon code is invalid', async () => { - sub.discount = 40; - sub.key = 'google_6mo'; - coupon = 'example-coupon'; - - const couponModel = new Coupon(); - couponModel.event = 'google_6mo'; - await couponModel.save(); - - sinon.stub(cc, 'validate').returns('invalid'); - - await expect(amzLib.subscribe({ - billingAgreementId, - sub, - coupon, - user, - groupId, - headers, - })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 401, - name: 'NotAuthorized', - message: i18n.t('invalidCoupon'), - }); - cc.validate.restore(); - }); - - it('subscribes with amazon with a coupon', async () => { - sub.discount = 40; - sub.key = 'google_6mo'; - coupon = 'example-coupon'; - - const couponModel = new Coupon(); - couponModel.event = 'google_6mo'; - const updatedCouponModel = await couponModel.save(); - - sinon.stub(cc, 'validate').returns(updatedCouponModel._id); - - await amzLib.subscribe({ - billingAgreementId, - sub, - coupon, - user, - groupId, - headers, - }); - - expectCreateSpy(); - - cc.validate.restore(); - }); - - it('subscribes with amazon', async () => { - user.guilds.push(groupId); - await user.save(); - - await amzLib.subscribe({ - billingAgreementId, - sub, - coupon, - user, - groupId, - headers, - }); - - expectAmazonSetBillingAgreementDetailsSpy(); - - expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce; - expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - }); - - expectAmazonAuthorizeBillingAgreementSpy(); - - expectCreateSpy(); - }); - - it('subscribes with amazon with price to existing users', async () => { - user = new User(); - user.guilds.push(groupId); - await user.save(); - - // Add existing users - user = new User(); - user.guilds.push(groupId); - await user.save(); - - // Set expected amount - sub.key = 'group_monthly'; - sub.price = 9; - amount = 12; - - await amzLib.subscribe({ - billingAgreementId, - sub, - coupon, - user, - groupId, - headers, - }); - - expectAmazonSetBillingAgreementDetailsSpy(); - expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce; - expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - }); - expectAmazonAuthorizeBillingAgreementSpy(); - expectCreateSpy(); - }); -}); diff --git a/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js b/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js deleted file mode 100644 index 26cec748355..00000000000 --- a/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js +++ /dev/null @@ -1,86 +0,0 @@ -import { - generateGroup, -} from '../../../../../helpers/api-unit.helper'; -import { model as User } from '../../../../../../website/server/models/user'; -import { model as Group } from '../../../../../../website/server/models/group'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; -import payments from '../../../../../../website/server/libs/payments/payments'; -import common from '../../../../../../website/common'; - -describe('#upgradeGroupPlan', () => { - let spy; let data; let user; let group; let - uuidString; - - beforeEach(async () => { - user = new User(); - user.profile.name = 'sender'; - - data = { - user, - sub: { - key: 'basic_3mo', // @TODO: Validate that this is group - }, - customerId: 'customer-id', - paymentMethod: 'Payment Method', - headers: { - 'x-client': 'habitica-web', - 'user-agent': '', - }, - }; - - group = generateGroup({ - name: 'test group', - type: 'guild', - privacy: 'private', - leader: user._id, - }); - await group.save(); - - user.guilds.push(group._id); - await user.save(); - - spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement'); - spy.resolves([]); - - uuidString = 'uuid-v4'; - sinon.stub(common, 'uuid').returns(uuidString); - - data.groupId = group._id; - data.sub.quantity = 3; - }); - - afterEach(() => { - amzLib.authorizeOnBillingAgreement.restore(); - common.uuid.restore(); - }); - - it('charges for a new member', async () => { - data.paymentMethod = amzLib.constants.PAYMENT_METHOD; - await payments.createSubscription(data); - - const updatedGroup = await Group.findById(group._id).exec(); - - updatedGroup.memberCount += 1; - await updatedGroup.save(); - - await amzLib.chargeForAdditionalGroupMember(updatedGroup); - - expect(spy.calledOnce).to.be.true; - expect(spy).to.be.calledWith({ - AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId, - AuthorizationReferenceId: uuidString.substring(0, 32), - AuthorizationAmount: { - CurrencyCode: amzLib.constants.CURRENCY_CODE, - Amount: 3, - }, - SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER, - TransactionTimeout: 0, - CaptureNow: true, - SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER, - SellerOrderAttributes: { - SellerOrderId: uuidString, - StoreName: amzLib.constants.STORE_NAME, - }, - }); - }); -}); diff --git a/test/api/unit/libs/payments/group-plans/group-payments-create.test.js b/test/api/unit/libs/payments/group-plans/group-payments-create.test.js index 17ff154a0a0..1e3f0336dc3 100644 --- a/test/api/unit/libs/payments/group-plans/group-payments-create.test.js +++ b/test/api/unit/libs/payments/group-plans/group-payments-create.test.js @@ -4,7 +4,6 @@ import nconf from 'nconf'; import * as sender from '../../../../../../website/server/libs/email'; import api from '../../../../../../website/server/libs/payments/payments'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; import stripePayments from '../../../../../../website/server/libs/payments/stripe'; import { model as User } from '../../../../../../website/server/models/user'; @@ -266,52 +265,6 @@ describe('Purchasing a group plan for group', () => { expect(leaderCall.args[1]).to.equal('group-subscription-begins'); }); - it('sends one email to subscribed member of group, stating subscription is cancelled (Amazon)', async () => { - sinon.stub(amzLib, 'getBillingAgreementDetails') - .resolves({ - BillingAgreementDetails: { - BillingAgreementStatus: { State: 'Closed' }, - }, - }); - - const recipient = new User(); - recipient.profile.name = 'recipient'; - plan.planId = 'basic_earned'; - plan.paymentMethod = amzLib.constants.PAYMENT_METHOD; - recipient.purchased.plan = plan; - recipient.guilds.push(group._id); - await recipient.save(); - - data.groupId = group._id; - - await api.createSubscription(data); - - expect(sender.sendTxn).to.be.calledThrice; - const recipientCall = sender.sendTxn.getCalls().find(call => { - const isRecipient = call.args[0]._id === recipient._id; - const isJoin = call.args[1] === 'group-member-join'; - return isRecipient && isJoin; - }); - expect(recipientCall.args[0]._id).to.equal(recipient._id); - expect(recipientCall.args[1]).to.equal('group-member-join'); - expect(recipientCall.args[2]).to.eql([ - { name: 'LEADER', content: user.profile.name }, - { name: 'GROUP_NAME', content: group.name }, - { name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL }, - ]); - - // confirm that the other email sent is not a cancel-subscription email: - const leaderCall = sender.sendTxn.getCalls().find(call => { - const isLeader = call.args[0]._id === group.leader; - const isSubscriptionBegin = call.args[1] === 'group-subscription-begins'; - return isLeader && isSubscriptionBegin; - }); - expect(leaderCall.args[0]._id).to.equal(group.leader); - expect(leaderCall.args[1]).to.equal('group-subscription-begins'); - - amzLib.getBillingAgreementDetails.restore(); - }); - it('sends one email to subscribed member of group, stating subscription is cancelled (PayPal)', async () => { sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').resolves({}); sinon.stub(paypalPayments, 'paypalBillingAgreementGet') @@ -553,33 +506,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3); }); - it('adds months to members with existing recurring subscription (Amazon)', async () => { - sinon.stub(amzLib, 'getBillingAgreementDetails') - .resolves({ - BillingAgreementDetails: { - BillingAgreementStatus: { State: 'Closed' }, - }, - }); - - const recipient = new User(); - recipient.profile.name = 'recipient'; - plan.planId = 'basic_earned'; - plan.paymentMethod = amzLib.constants.PAYMENT_METHOD; - plan.lastBillingDate = moment().add(3, 'months'); - recipient.purchased.plan = plan; - recipient.guilds.push(group._id); - - await recipient.save(); - - data.groupId = group._id; - - await api.createSubscription(data); - - const updatedUser = await User.findById(recipient._id).exec(); - - expect(updatedUser.purchased.plan.extraMonths).to.within(3, 5); - }); - it('adds months to members with existing recurring subscription (Paypal)', async () => { sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').resolves({}); sinon.stub(paypalPayments, 'paypalBillingAgreementGet') diff --git a/test/api/unit/libs/payments/payments.test.js b/test/api/unit/libs/payments/payments.test.js index 00732af3cea..7d7af4ac4df 100644 --- a/test/api/unit/libs/payments/payments.test.js +++ b/test/api/unit/libs/payments/payments.test.js @@ -520,14 +520,6 @@ describe('payments/index', () => { expect(user.purchased.plan.gemsBought).to.eql(10); }); - it('sets lastBillingDate if payment method is "Amazon Payments"', async () => { - data.paymentMethod = 'Amazon Payments'; - - await api.createSubscription(data); - - expect(user.purchased.plan.lastBillingDate).to.exist; - }); - it('increases the user\'s transaction count', async () => { expect(user.purchased.txnCount).to.eql(0); diff --git a/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js deleted file mode 100644 index 7c2466f0f2c..00000000000 --- a/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js +++ /dev/null @@ -1,78 +0,0 @@ -import { - createAndPopulateGroup, - generateUser, - translate as t, -} from '../../../../../helpers/api-integration/v3'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; - -describe('payments : amazon #subscribeCancel', () => { - const endpoint = '/amazon/subscribe/cancel?noRedirect=true'; - let user; let group; let - amazonSubscribeCancelStub; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('throws error when there users has no subscription', async () => { - await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('missingSubscription'), - }); - }); - - describe('success', () => { - beforeEach(() => { - amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').resolves({}); - }); - - afterEach(() => { - amzLib.cancelSubscription.restore(); - }); - - it('cancels a user subscription', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - await user.get(endpoint); - - expect(amazonSubscribeCancelStub).to.be.calledOnce; - expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); - expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(undefined); - expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - - it('cancels a group subscription', async () => { - ({ group, groupLeader: user } = await createAndPopulateGroup({ - groupDetails: { - name: 'test group', - type: 'guild', - privacy: 'private', - }, - leaderDetails: { - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }, - upgradeToGroupPlan: true, - })); - - await user.get(`${endpoint}&groupId=${group._id}`); - - expect(amazonSubscribeCancelStub).to.be.calledOnce; - expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); - expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(group._id); - expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js deleted file mode 100644 index 6774317c9ca..00000000000 --- a/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js +++ /dev/null @@ -1,64 +0,0 @@ -import { - generateUser, -} from '../../../../../helpers/api-integration/v3'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; - -describe('payments - amazon - #checkout', () => { - const endpoint = '/amazon/checkout'; - let user; let - amazonCheckoutStub; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies credentials', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: 'Missing req.body.orderReferenceId', - }); - }); - - describe('success', () => { - beforeEach(async () => { - amazonCheckoutStub = sinon.stub(amzLib, 'checkout').resolves({}); - }); - - afterEach(() => { - amzLib.checkout.restore(); - }); - - it('makes a purchase with amazon checkout', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - const gift = { - type: 'gems', - gems: { - amount: 16, - uuid: user._id, - }, - }; - - const orderReferenceId = 'orderReferenceId-example'; - - await user.post(endpoint, { - gift, - orderReferenceId, - }); - - expect(amazonCheckoutStub).to.be.calledOnce; - expect(amazonCheckoutStub.args[0][0].user._id).to.eql(user._id); - expect(amazonCheckoutStub.args[0][0].gift).to.eql(gift); - expect(amazonCheckoutStub.args[0][0].orderReferenceId).to.eql(orderReferenceId); - expect(amazonCheckoutStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(amazonCheckoutStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js deleted file mode 100644 index d9652534266..00000000000 --- a/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - generateUser, -} from '../../../../../helpers/api-integration/v3'; - -describe('payments - amazon - #createOrderReferenceId', () => { - const endpoint = '/amazon/createOrderReferenceId'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies billingAgreementId', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: 'Missing req.body.billingAgreementId', - }); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js deleted file mode 100644 index 3ce1a718e29..00000000000 --- a/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js +++ /dev/null @@ -1,97 +0,0 @@ -import { - generateUser, - generateGroup, - translate as t, -} from '../../../../../helpers/api-integration/v3'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; - -describe('payments - amazon - #subscribe', () => { - const endpoint = '/amazon/subscribe'; - let user; let group; let - subscribeWithAmazonStub; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies subscription code', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('missingSubscriptionCode'), - }); - }); - - describe('success', () => { - const billingAgreementId = 'billingAgreementId-example'; - const subscription = 'basic_3mo'; - let coupon; - - beforeEach(() => { - subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').resolves({}); - }); - - afterEach(() => { - amzLib.subscribe.restore(); - }); - - it('creates a user subscription', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - await user.post(endpoint, { - billingAgreementId, - subscription, - coupon, - }); - - expect(subscribeWithAmazonStub).to.be.calledOnce; - expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId); - expect(subscribeWithAmazonStub.args[0][0].sub).to.exist; - expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon); - expect(subscribeWithAmazonStub.args[0][0].groupId).not.exist; - expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - - it('creates a group subscription', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - group = await generateGroup(user, { - name: 'test group', - type: 'party', - privacy: 'private', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - }); - - await user.post(endpoint, { - billingAgreementId, - subscription, - coupon, - groupId: group._id, - }); - - expect(subscribeWithAmazonStub).to.be.calledOnce; - expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId); - expect(subscribeWithAmazonStub.args[0][0].sub).to.exist; - expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon); - expect(subscribeWithAmazonStub.args[0][0].user._id).to.eql(user._id); - expect(subscribeWithAmazonStub.args[0][0].groupId).to.eql(group._id); - expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js deleted file mode 100644 index e7e0b81d655..00000000000 --- a/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - generateUser, -} from '../../../../../helpers/api-integration/v3'; - -describe('payments : amazon', () => { - const endpoint = '/amazon/verifyAccessToken'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies access token', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: 'Missing req.body.access_token', - }); - }); -}); diff --git a/test/helpers/api-integration/requester.js b/test/helpers/api-integration/requester.js index 61c0b2fabf2..881f0b9ec5f 100644 --- a/test/helpers/api-integration/requester.js +++ b/test/helpers/api-integration/requester.js @@ -38,7 +38,6 @@ function _requestMaker (user, method, additionalSets = {}) { route.indexOf('/email') === 0 || route.indexOf('/export') === 0 || route.indexOf('/paypal') === 0 - || route.indexOf('/amazon') === 0 || route.indexOf('/stripe') === 0 || route.indexOf('/analytics') === 0 ) { diff --git a/website/client/vue.config.js b/website/client/vue.config.js index e34e6d0c018..3783d0cedc3 100644 --- a/website/client/vue.config.js +++ b/website/client/vue.config.js @@ -15,9 +15,6 @@ setupNconf(configFile, nconf); const DEV_BASE_URL = nconf.get('BASE_URL'); const envVars = [ - 'AMAZON_PAYMENTS_SELLER_ID', - 'AMAZON_PAYMENTS_CLIENT_ID', - 'AMAZON_PAYMENTS_MODE', 'EMAILS_COMMUNITY_MANAGER_EMAIL', 'EMAILS_TECH_ASSISTANCE_EMAIL', 'EMAILS_PRESS_ENQUIRY_EMAIL', @@ -174,10 +171,6 @@ module.exports = { target: DEV_BASE_URL, changeOrigin: true, }, - '^/amazon': { - target: DEV_BASE_URL, - changeOrigin: true, - }, '^/paypal': { target: DEV_BASE_URL, changeOrigin: true, From 82bf265c70d8fa073d866ac15c099342ae7df8c4 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Tue, 3 Dec 2024 14:53:49 -0500 Subject: [PATCH 19/24] refactor(app): fix api unit test --- habitica-images | 2 +- package-lock.json | 6 + package.json | 1 + .../unit/libs/payments/amazon/cancel.test.js | 180 ++++++++++++++++++ .../group-plans/group-payments-cancel.test.js | 2 - .../group-plans/group-payments-create.test.js | 11 -- test/api/unit/libs/payments/payments.test.js | 3 - ...T-payments_amazon_subscribe_cancel.test.js | 78 ++++++++ .../POST-payments_amazon_checkout.test.js | 64 +++++++ ...ents_amazon_createOrderReferenceId.test.js | 20 ++ .../POST-payments_amazon_subscribe.test.js | 97 ++++++++++ ...-payments_amazon_verifyAccessToken.test.js | 20 ++ 12 files changed, 467 insertions(+), 17 deletions(-) create mode 100644 test/api/unit/libs/payments/amazon/cancel.test.js create mode 100644 test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js create mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js create mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js create mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js create mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js diff --git a/habitica-images b/habitica-images index d2919bc15f3..47909bcc2d1 160000 --- a/habitica-images +++ b/habitica-images @@ -1 +1 @@ -Subproject commit d2919bc15f38d7bd90c884447981cd5bcaaf6739 +Subproject commit 47909bcc2d12a126fe59d4046432a7762a3fa2c3 diff --git a/package-lock.json b/package-lock.json index d18448792be..7a396ce301a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@parse/node-apn": "^5.2.3", "@slack/webhook": "^6.1.0", "accepts": "^1.3.8", + "amazon": "^0.0.0", "amazon-payments": "^0.2.9", "amplitude": "^6.0.0", "apidoc": "^0.54.0", @@ -3990,6 +3991,11 @@ "ajv": "^6.9.1" } }, + "node_modules/amazon": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/amazon/-/amazon-0.0.0.tgz", + "integrity": "sha512-ajpn2cnNz5acb51P8kexxs9YSltR2ut3jW8kOdlRIeqhX9K4598TqUuKnJqDw4RePqlFZZAxnYAVGbZ7J5phsA==" + }, "node_modules/amazon-payments": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/amazon-payments/-/amazon-payments-0.2.9.tgz", diff --git a/package.json b/package.json index efd399bb421..187543936d4 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@parse/node-apn": "^5.2.3", "@slack/webhook": "^6.1.0", "accepts": "^1.3.8", + "amazon": "^0.0.0", "amazon-payments": "^0.2.9", "amplitude": "^6.0.0", "apidoc": "^0.54.0", diff --git a/test/api/unit/libs/payments/amazon/cancel.test.js b/test/api/unit/libs/payments/amazon/cancel.test.js new file mode 100644 index 00000000000..e24d3dbf522 --- /dev/null +++ b/test/api/unit/libs/payments/amazon/cancel.test.js @@ -0,0 +1,180 @@ +import moment from 'moment'; + +import { + generateGroup, +} from '../../../../../helpers/api-unit.helper'; +import { model as User } from '../../../../../../website/server/models/user'; +import amzLib from '../../../../../../website/server/libs/payments/amazon'; +import payments from '../../../../../../website/server/libs/payments/payments'; +import common from '../../../../../../website/common'; +import { createNonLeaderGroupMember } from '../paymentHelpers'; + +const { i18n } = common; + +describe('Amazon Payments - Cancel Subscription', () => { + const subKey = 'basic_3mo'; + + let user; let group; let headers; let billingAgreementId; let subscriptionBlock; let + subscriptionLength; + let getBillingAgreementDetailsSpy; + let paymentCancelSubscriptionSpy; + + function expectAmazonStubs () { + expect(getBillingAgreementDetailsSpy).to.be.calledOnce; + expect(getBillingAgreementDetailsSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + } + + function expectAmazonCancelSubscriptionSpy (groupId, lastBillingDate) { + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + groupId, + nextBill: moment(lastBillingDate).add({ days: subscriptionLength }), + paymentMethod: amzLib.constants.PAYMENT_METHOD, + headers, + cancellationReason: undefined, + }); + } + + function expectAmazonCancelUserSubscriptionSpy () { + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expectAmazonCancelSubscriptionSpy(undefined, user.purchased.plan.lastBillingDate); + } + + function expectAmazonCancelGroupSubscriptionSpy (groupId) { + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expectAmazonCancelSubscriptionSpy(groupId, group.purchased.plan.lastBillingDate); + } + + function expectBillingAggreementDetailSpy () { + getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails') + .resolves({ + BillingAgreementDetails: { + BillingAgreementStatus: { State: 'Open' }, + }, + }); + } + + beforeEach(async () => { + user = new User(); + user.profile.name = 'sender'; + user.purchased.plan.customerId = 'customer-id'; + user.purchased.plan.planId = subKey; + user.purchased.plan.lastBillingDate = new Date(); + + group = generateGroup({ + name: 'test group', + type: 'guild', + privacy: 'public', + leader: user._id, + }); + group.purchased.plan.customerId = 'customer-id'; + group.purchased.plan.planId = subKey; + group.purchased.plan.lastBillingDate = new Date(); + await group.save(); + + subscriptionBlock = common.content.subscriptionBlocks[subKey]; + subscriptionLength = subscriptionBlock.months * 30; + + headers = {}; + + getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails'); + getBillingAgreementDetailsSpy.resolves({ + BillingAgreementDetails: { + BillingAgreementStatus: { State: 'Closed' }, + }, + }); + + paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription'); + paymentCancelSubscriptionSpy.resolves({}); + }); + + afterEach(() => { + amzLib.getBillingAgreementDetails.restore(); + payments.cancelSubscription.restore(); + }); + + it('should throw an error if we are missing a subscription', async () => { + user.purchased.plan.customerId = undefined; + + await expect(amzLib.cancelSubscription({ user })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('missingSubscription'), + }); + }); + + it('should cancel a user subscription', async () => { + billingAgreementId = user.purchased.plan.customerId; + + await amzLib.cancelSubscription({ user, headers }); + + expectAmazonCancelUserSubscriptionSpy(); + expectAmazonStubs(); + }); + + it('should close a user subscription if amazon not closed', async () => { + amzLib.getBillingAgreementDetails.restore(); + expectBillingAggreementDetailSpy(); + const closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); + billingAgreementId = user.purchased.plan.customerId; + + await amzLib.cancelSubscription({ user, headers }); + + expectAmazonStubs(); + expect(closeBillingAgreementSpy).to.be.calledOnce; + expect(closeBillingAgreementSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + expectAmazonCancelUserSubscriptionSpy(); + amzLib.closeBillingAgreement.restore(); + }); + + it('should throw an error if group is not found', async () => { + await expect(amzLib.cancelSubscription({ user, groupId: 'fake-id' })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 404, + name: 'NotFound', + message: i18n.t('groupNotFound'), + }); + }); + + it('should throw an error if user is not group leader', async () => { + const nonLeader = await createNonLeaderGroupMember(group); + + await expect(amzLib.cancelSubscription({ user: nonLeader, groupId: group._id })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('onlyGroupLeaderCanManageSubscription'), + }); + }); + + it('should cancel a group subscription', async () => { + billingAgreementId = group.purchased.plan.customerId; + + await amzLib.cancelSubscription({ user, groupId: group._id, headers }); + + expectAmazonCancelGroupSubscriptionSpy(group._id); + expectAmazonStubs(); + }); + + it('should close a group subscription if amazon not closed', async () => { + amzLib.getBillingAgreementDetails.restore(); + expectBillingAggreementDetailSpy(); + const closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); + billingAgreementId = group.purchased.plan.customerId; + + await amzLib.cancelSubscription({ user, groupId: group._id, headers }); + + expectAmazonStubs(); + expect(closeBillingAgreementSpy).to.be.calledOnce; + expect(closeBillingAgreementSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + expectAmazonCancelGroupSubscriptionSpy(group._id); + amzLib.closeBillingAgreement.restore(); + }); +}); diff --git a/test/api/unit/libs/payments/group-plans/group-payments-cancel.test.js b/test/api/unit/libs/payments/group-plans/group-payments-cancel.test.js index 49cb80d4e3a..0dc192fb633 100644 --- a/test/api/unit/libs/payments/group-plans/group-payments-cancel.test.js +++ b/test/api/unit/libs/payments/group-plans/group-payments-cancel.test.js @@ -290,7 +290,6 @@ describe('Canceling a subscription for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated); }); @@ -329,7 +328,6 @@ describe('Canceling a subscription for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond); expect(updatedUser.purchased.plan.dateTerminated).to.exist; - expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated); }); }); diff --git a/test/api/unit/libs/payments/group-plans/group-payments-create.test.js b/test/api/unit/libs/payments/group-plans/group-payments-create.test.js index 1e3f0336dc3..50a1ba3eae8 100644 --- a/test/api/unit/libs/payments/group-plans/group-payments-create.test.js +++ b/test/api/unit/libs/payments/group-plans/group-payments-create.test.js @@ -62,7 +62,6 @@ describe('Purchasing a group plan for group', () => { paymentMethod: 'paymentMethod', extraMonths: 0, dateTerminated: null, - lastBillingDate: new Date(), dateCreated: new Date(), mysteryItems: [], consecutive: { @@ -111,7 +110,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method'); expect(updatedGroup.purchased.plan.extraMonths).to.eql(0); expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null); - expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist; expect(updatedGroup.purchased.plan.dateCreated).to.exist; }); @@ -188,7 +186,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedLeader.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedLeader.purchased.plan.extraMonths).to.eql(0); expect(updatedLeader.purchased.plan.dateTerminated).to.eql(null); - expect(updatedLeader.purchased.plan.lastBillingDate).to.not.exist; expect(updatedLeader.purchased.plan.dateCreated).to.exist; expect(updatedLeader.items.mounts['Jackalope-RoyalPurple']).to.be.true; @@ -451,7 +448,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.within(1, 3); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.exist; }); @@ -485,7 +481,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.within(3, 5); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.exist; }); @@ -715,7 +710,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated); }); @@ -765,7 +759,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated); }); @@ -792,7 +785,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('paymentMethod'); expect(updatedUser.purchased.plan.extraMonths).to.eql(0); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.exist; expect(updatedUser.purchased.plan.dateCreated).to.exist; }); @@ -819,7 +811,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.GOOGLE_PAYMENT_METHOD); expect(updatedUser.purchased.plan.extraMonths).to.eql(0); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.exist; expect(updatedUser.purchased.plan.dateCreated).to.exist; }); @@ -846,7 +837,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.IOS_PAYMENT_METHOD); expect(updatedUser.purchased.plan.extraMonths).to.eql(0); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.exist; expect(updatedUser.purchased.plan.dateCreated).to.exist; }); @@ -874,7 +864,6 @@ describe('Purchasing a group plan for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.within(0, 2); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.exist; }); }); diff --git a/test/api/unit/libs/payments/payments.test.js b/test/api/unit/libs/payments/payments.test.js index 7d7af4ac4df..23aa8a18ddd 100644 --- a/test/api/unit/libs/payments/payments.test.js +++ b/test/api/unit/libs/payments/payments.test.js @@ -61,7 +61,6 @@ describe('payments/index', () => { paymentMethod: 'paymentMethod', extraMonths: 0, dateTerminated: null, - lastBillingDate: new Date(), dateCreated: new Date(), mysteryItems: [], consecutive: { @@ -451,7 +450,6 @@ describe('payments/index', () => { expect(user.purchased.plan.paymentMethod).to.eql('Payment Method'); expect(user.purchased.plan.extraMonths).to.eql(0); expect(user.purchased.plan.dateTerminated).to.eql(null); - expect(user.purchased.plan.lastBillingDate).to.not.exist; expect(user.purchased.plan.dateCreated).to.exist; }); @@ -1383,7 +1381,6 @@ describe('payments/index', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.eql(0); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); - expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.exist; }); diff --git a/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js new file mode 100644 index 00000000000..7c2466f0f2c --- /dev/null +++ b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js @@ -0,0 +1,78 @@ +import { + createAndPopulateGroup, + generateUser, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import amzLib from '../../../../../../website/server/libs/payments/amazon'; + +describe('payments : amazon #subscribeCancel', () => { + const endpoint = '/amazon/subscribe/cancel?noRedirect=true'; + let user; let group; let + amazonSubscribeCancelStub; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('throws error when there users has no subscription', async () => { + await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('missingSubscription'), + }); + }); + + describe('success', () => { + beforeEach(() => { + amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').resolves({}); + }); + + afterEach(() => { + amzLib.cancelSubscription.restore(); + }); + + it('cancels a user subscription', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + await user.get(endpoint); + + expect(amazonSubscribeCancelStub).to.be.calledOnce; + expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); + expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(undefined); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + + it('cancels a group subscription', async () => { + ({ group, groupLeader: user } = await createAndPopulateGroup({ + groupDetails: { + name: 'test group', + type: 'guild', + privacy: 'private', + }, + leaderDetails: { + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }, + upgradeToGroupPlan: true, + })); + + await user.get(`${endpoint}&groupId=${group._id}`); + + expect(amazonSubscribeCancelStub).to.be.calledOnce; + expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); + expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(group._id); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + }); +}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js new file mode 100644 index 00000000000..6774317c9ca --- /dev/null +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js @@ -0,0 +1,64 @@ +import { + generateUser, +} from '../../../../../helpers/api-integration/v3'; +import amzLib from '../../../../../../website/server/libs/payments/amazon'; + +describe('payments - amazon - #checkout', () => { + const endpoint = '/amazon/checkout'; + let user; let + amazonCheckoutStub; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies credentials', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'Missing req.body.orderReferenceId', + }); + }); + + describe('success', () => { + beforeEach(async () => { + amazonCheckoutStub = sinon.stub(amzLib, 'checkout').resolves({}); + }); + + afterEach(() => { + amzLib.checkout.restore(); + }); + + it('makes a purchase with amazon checkout', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + const gift = { + type: 'gems', + gems: { + amount: 16, + uuid: user._id, + }, + }; + + const orderReferenceId = 'orderReferenceId-example'; + + await user.post(endpoint, { + gift, + orderReferenceId, + }); + + expect(amazonCheckoutStub).to.be.calledOnce; + expect(amazonCheckoutStub.args[0][0].user._id).to.eql(user._id); + expect(amazonCheckoutStub.args[0][0].gift).to.eql(gift); + expect(amazonCheckoutStub.args[0][0].orderReferenceId).to.eql(orderReferenceId); + expect(amazonCheckoutStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(amazonCheckoutStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + }); +}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js new file mode 100644 index 00000000000..d9652534266 --- /dev/null +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js @@ -0,0 +1,20 @@ +import { + generateUser, +} from '../../../../../helpers/api-integration/v3'; + +describe('payments - amazon - #createOrderReferenceId', () => { + const endpoint = '/amazon/createOrderReferenceId'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies billingAgreementId', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'Missing req.body.billingAgreementId', + }); + }); +}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js new file mode 100644 index 00000000000..3ce1a718e29 --- /dev/null +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js @@ -0,0 +1,97 @@ +import { + generateUser, + generateGroup, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import amzLib from '../../../../../../website/server/libs/payments/amazon'; + +describe('payments - amazon - #subscribe', () => { + const endpoint = '/amazon/subscribe'; + let user; let group; let + subscribeWithAmazonStub; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies subscription code', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('missingSubscriptionCode'), + }); + }); + + describe('success', () => { + const billingAgreementId = 'billingAgreementId-example'; + const subscription = 'basic_3mo'; + let coupon; + + beforeEach(() => { + subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').resolves({}); + }); + + afterEach(() => { + amzLib.subscribe.restore(); + }); + + it('creates a user subscription', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + await user.post(endpoint, { + billingAgreementId, + subscription, + coupon, + }); + + expect(subscribeWithAmazonStub).to.be.calledOnce; + expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId); + expect(subscribeWithAmazonStub.args[0][0].sub).to.exist; + expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon); + expect(subscribeWithAmazonStub.args[0][0].groupId).not.exist; + expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + + it('creates a group subscription', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + group = await generateGroup(user, { + name: 'test group', + type: 'party', + privacy: 'private', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + }); + + await user.post(endpoint, { + billingAgreementId, + subscription, + coupon, + groupId: group._id, + }); + + expect(subscribeWithAmazonStub).to.be.calledOnce; + expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId); + expect(subscribeWithAmazonStub.args[0][0].sub).to.exist; + expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon); + expect(subscribeWithAmazonStub.args[0][0].user._id).to.eql(user._id); + expect(subscribeWithAmazonStub.args[0][0].groupId).to.eql(group._id); + expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + }); +}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js new file mode 100644 index 00000000000..e7e0b81d655 --- /dev/null +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js @@ -0,0 +1,20 @@ +import { + generateUser, +} from '../../../../../helpers/api-integration/v3'; + +describe('payments : amazon', () => { + const endpoint = '/amazon/verifyAccessToken'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies access token', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'Missing req.body.access_token', + }); + }); +}); From afdd43e32ae35a6fc851f893666392463d31b455 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Tue, 3 Dec 2024 14:55:55 -0500 Subject: [PATCH 20/24] refactor(app): remove amazon tests --- .../unit/libs/payments/amazon/cancel.test.js | 180 ------------------ ...T-payments_amazon_subscribe_cancel.test.js | 78 -------- .../POST-payments_amazon_checkout.test.js | 64 ------- ...ents_amazon_createOrderReferenceId.test.js | 20 -- .../POST-payments_amazon_subscribe.test.js | 97 ---------- ...-payments_amazon_verifyAccessToken.test.js | 20 -- 6 files changed, 459 deletions(-) delete mode 100644 test/api/unit/libs/payments/amazon/cancel.test.js delete mode 100644 test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js delete mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js delete mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js delete mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js delete mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js diff --git a/test/api/unit/libs/payments/amazon/cancel.test.js b/test/api/unit/libs/payments/amazon/cancel.test.js deleted file mode 100644 index e24d3dbf522..00000000000 --- a/test/api/unit/libs/payments/amazon/cancel.test.js +++ /dev/null @@ -1,180 +0,0 @@ -import moment from 'moment'; - -import { - generateGroup, -} from '../../../../../helpers/api-unit.helper'; -import { model as User } from '../../../../../../website/server/models/user'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; -import payments from '../../../../../../website/server/libs/payments/payments'; -import common from '../../../../../../website/common'; -import { createNonLeaderGroupMember } from '../paymentHelpers'; - -const { i18n } = common; - -describe('Amazon Payments - Cancel Subscription', () => { - const subKey = 'basic_3mo'; - - let user; let group; let headers; let billingAgreementId; let subscriptionBlock; let - subscriptionLength; - let getBillingAgreementDetailsSpy; - let paymentCancelSubscriptionSpy; - - function expectAmazonStubs () { - expect(getBillingAgreementDetailsSpy).to.be.calledOnce; - expect(getBillingAgreementDetailsSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - }); - } - - function expectAmazonCancelSubscriptionSpy (groupId, lastBillingDate) { - expect(paymentCancelSubscriptionSpy).to.be.calledWith({ - user, - groupId, - nextBill: moment(lastBillingDate).add({ days: subscriptionLength }), - paymentMethod: amzLib.constants.PAYMENT_METHOD, - headers, - cancellationReason: undefined, - }); - } - - function expectAmazonCancelUserSubscriptionSpy () { - expect(paymentCancelSubscriptionSpy).to.be.calledOnce; - expectAmazonCancelSubscriptionSpy(undefined, user.purchased.plan.lastBillingDate); - } - - function expectAmazonCancelGroupSubscriptionSpy (groupId) { - expect(paymentCancelSubscriptionSpy).to.be.calledOnce; - expectAmazonCancelSubscriptionSpy(groupId, group.purchased.plan.lastBillingDate); - } - - function expectBillingAggreementDetailSpy () { - getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails') - .resolves({ - BillingAgreementDetails: { - BillingAgreementStatus: { State: 'Open' }, - }, - }); - } - - beforeEach(async () => { - user = new User(); - user.profile.name = 'sender'; - user.purchased.plan.customerId = 'customer-id'; - user.purchased.plan.planId = subKey; - user.purchased.plan.lastBillingDate = new Date(); - - group = generateGroup({ - name: 'test group', - type: 'guild', - privacy: 'public', - leader: user._id, - }); - group.purchased.plan.customerId = 'customer-id'; - group.purchased.plan.planId = subKey; - group.purchased.plan.lastBillingDate = new Date(); - await group.save(); - - subscriptionBlock = common.content.subscriptionBlocks[subKey]; - subscriptionLength = subscriptionBlock.months * 30; - - headers = {}; - - getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails'); - getBillingAgreementDetailsSpy.resolves({ - BillingAgreementDetails: { - BillingAgreementStatus: { State: 'Closed' }, - }, - }); - - paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription'); - paymentCancelSubscriptionSpy.resolves({}); - }); - - afterEach(() => { - amzLib.getBillingAgreementDetails.restore(); - payments.cancelSubscription.restore(); - }); - - it('should throw an error if we are missing a subscription', async () => { - user.purchased.plan.customerId = undefined; - - await expect(amzLib.cancelSubscription({ user })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 401, - name: 'NotAuthorized', - message: i18n.t('missingSubscription'), - }); - }); - - it('should cancel a user subscription', async () => { - billingAgreementId = user.purchased.plan.customerId; - - await amzLib.cancelSubscription({ user, headers }); - - expectAmazonCancelUserSubscriptionSpy(); - expectAmazonStubs(); - }); - - it('should close a user subscription if amazon not closed', async () => { - amzLib.getBillingAgreementDetails.restore(); - expectBillingAggreementDetailSpy(); - const closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); - billingAgreementId = user.purchased.plan.customerId; - - await amzLib.cancelSubscription({ user, headers }); - - expectAmazonStubs(); - expect(closeBillingAgreementSpy).to.be.calledOnce; - expect(closeBillingAgreementSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - }); - expectAmazonCancelUserSubscriptionSpy(); - amzLib.closeBillingAgreement.restore(); - }); - - it('should throw an error if group is not found', async () => { - await expect(amzLib.cancelSubscription({ user, groupId: 'fake-id' })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 404, - name: 'NotFound', - message: i18n.t('groupNotFound'), - }); - }); - - it('should throw an error if user is not group leader', async () => { - const nonLeader = await createNonLeaderGroupMember(group); - - await expect(amzLib.cancelSubscription({ user: nonLeader, groupId: group._id })) - .to.eventually.be.rejected.and.to.eql({ - httpCode: 401, - name: 'NotAuthorized', - message: i18n.t('onlyGroupLeaderCanManageSubscription'), - }); - }); - - it('should cancel a group subscription', async () => { - billingAgreementId = group.purchased.plan.customerId; - - await amzLib.cancelSubscription({ user, groupId: group._id, headers }); - - expectAmazonCancelGroupSubscriptionSpy(group._id); - expectAmazonStubs(); - }); - - it('should close a group subscription if amazon not closed', async () => { - amzLib.getBillingAgreementDetails.restore(); - expectBillingAggreementDetailSpy(); - const closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); - billingAgreementId = group.purchased.plan.customerId; - - await amzLib.cancelSubscription({ user, groupId: group._id, headers }); - - expectAmazonStubs(); - expect(closeBillingAgreementSpy).to.be.calledOnce; - expect(closeBillingAgreementSpy).to.be.calledWith({ - AmazonBillingAgreementId: billingAgreementId, - }); - expectAmazonCancelGroupSubscriptionSpy(group._id); - amzLib.closeBillingAgreement.restore(); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js deleted file mode 100644 index 7c2466f0f2c..00000000000 --- a/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js +++ /dev/null @@ -1,78 +0,0 @@ -import { - createAndPopulateGroup, - generateUser, - translate as t, -} from '../../../../../helpers/api-integration/v3'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; - -describe('payments : amazon #subscribeCancel', () => { - const endpoint = '/amazon/subscribe/cancel?noRedirect=true'; - let user; let group; let - amazonSubscribeCancelStub; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('throws error when there users has no subscription', async () => { - await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('missingSubscription'), - }); - }); - - describe('success', () => { - beforeEach(() => { - amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').resolves({}); - }); - - afterEach(() => { - amzLib.cancelSubscription.restore(); - }); - - it('cancels a user subscription', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - await user.get(endpoint); - - expect(amazonSubscribeCancelStub).to.be.calledOnce; - expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); - expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(undefined); - expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - - it('cancels a group subscription', async () => { - ({ group, groupLeader: user } = await createAndPopulateGroup({ - groupDetails: { - name: 'test group', - type: 'guild', - privacy: 'private', - }, - leaderDetails: { - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }, - upgradeToGroupPlan: true, - })); - - await user.get(`${endpoint}&groupId=${group._id}`); - - expect(amazonSubscribeCancelStub).to.be.calledOnce; - expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); - expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(group._id); - expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js deleted file mode 100644 index 6774317c9ca..00000000000 --- a/test/api/v3/integration/payments/amazon/POST-payments_amazon_checkout.test.js +++ /dev/null @@ -1,64 +0,0 @@ -import { - generateUser, -} from '../../../../../helpers/api-integration/v3'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; - -describe('payments - amazon - #checkout', () => { - const endpoint = '/amazon/checkout'; - let user; let - amazonCheckoutStub; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies credentials', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: 'Missing req.body.orderReferenceId', - }); - }); - - describe('success', () => { - beforeEach(async () => { - amazonCheckoutStub = sinon.stub(amzLib, 'checkout').resolves({}); - }); - - afterEach(() => { - amzLib.checkout.restore(); - }); - - it('makes a purchase with amazon checkout', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - const gift = { - type: 'gems', - gems: { - amount: 16, - uuid: user._id, - }, - }; - - const orderReferenceId = 'orderReferenceId-example'; - - await user.post(endpoint, { - gift, - orderReferenceId, - }); - - expect(amazonCheckoutStub).to.be.calledOnce; - expect(amazonCheckoutStub.args[0][0].user._id).to.eql(user._id); - expect(amazonCheckoutStub.args[0][0].gift).to.eql(gift); - expect(amazonCheckoutStub.args[0][0].orderReferenceId).to.eql(orderReferenceId); - expect(amazonCheckoutStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(amazonCheckoutStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js deleted file mode 100644 index d9652534266..00000000000 --- a/test/api/v3/integration/payments/amazon/POST-payments_amazon_createOrderReferenceId.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - generateUser, -} from '../../../../../helpers/api-integration/v3'; - -describe('payments - amazon - #createOrderReferenceId', () => { - const endpoint = '/amazon/createOrderReferenceId'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies billingAgreementId', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: 'Missing req.body.billingAgreementId', - }); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js deleted file mode 100644 index 3ce1a718e29..00000000000 --- a/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js +++ /dev/null @@ -1,97 +0,0 @@ -import { - generateUser, - generateGroup, - translate as t, -} from '../../../../../helpers/api-integration/v3'; -import amzLib from '../../../../../../website/server/libs/payments/amazon'; - -describe('payments - amazon - #subscribe', () => { - const endpoint = '/amazon/subscribe'; - let user; let group; let - subscribeWithAmazonStub; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies subscription code', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('missingSubscriptionCode'), - }); - }); - - describe('success', () => { - const billingAgreementId = 'billingAgreementId-example'; - const subscription = 'basic_3mo'; - let coupon; - - beforeEach(() => { - subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').resolves({}); - }); - - afterEach(() => { - amzLib.subscribe.restore(); - }); - - it('creates a user subscription', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - await user.post(endpoint, { - billingAgreementId, - subscription, - coupon, - }); - - expect(subscribeWithAmazonStub).to.be.calledOnce; - expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId); - expect(subscribeWithAmazonStub.args[0][0].sub).to.exist; - expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon); - expect(subscribeWithAmazonStub.args[0][0].groupId).not.exist; - expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - - it('creates a group subscription', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - group = await generateGroup(user, { - name: 'test group', - type: 'party', - privacy: 'private', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - }); - - await user.post(endpoint, { - billingAgreementId, - subscription, - coupon, - groupId: group._id, - }); - - expect(subscribeWithAmazonStub).to.be.calledOnce; - expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId); - expect(subscribeWithAmazonStub.args[0][0].sub).to.exist; - expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon); - expect(subscribeWithAmazonStub.args[0][0].user._id).to.eql(user._id); - expect(subscribeWithAmazonStub.args[0][0].groupId).to.eql(group._id); - expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); - expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id); - }); - }); -}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js deleted file mode 100644 index e7e0b81d655..00000000000 --- a/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - generateUser, -} from '../../../../../helpers/api-integration/v3'; - -describe('payments : amazon', () => { - const endpoint = '/amazon/verifyAccessToken'; - let user; - - beforeEach(async () => { - user = await generateUser(); - }); - - it('verifies access token', async () => { - await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: 'Missing req.body.access_token', - }); - }); -}); From c8ef3041fd8f0f3387a0209b0811830d030a037d Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Wed, 4 Dec 2024 12:46:36 -0500 Subject: [PATCH 21/24] refactor(app): Update packages and remove commented code in payments.js --- package-lock.json | 36 ----------------------------- package.json | 2 -- website/client/src/libs/payments.js | 4 ---- 3 files changed, 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a396ce301a..d70696cccde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,6 @@ "@parse/node-apn": "^5.2.3", "@slack/webhook": "^6.1.0", "accepts": "^1.3.8", - "amazon": "^0.0.0", - "amazon-payments": "^0.2.9", "amplitude": "^6.0.0", "apidoc": "^0.54.0", "apple-auth": "^1.0.9", @@ -3991,32 +3989,6 @@ "ajv": "^6.9.1" } }, - "node_modules/amazon": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/amazon/-/amazon-0.0.0.tgz", - "integrity": "sha512-ajpn2cnNz5acb51P8kexxs9YSltR2ut3jW8kOdlRIeqhX9K4598TqUuKnJqDw4RePqlFZZAxnYAVGbZ7J5phsA==" - }, - "node_modules/amazon-payments": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/amazon-payments/-/amazon-payments-0.2.9.tgz", - "integrity": "sha512-Vy5o/maYVCLSGvrPlUvoCunVffSHawN8zCJSQZcYPfqnDsLvQl8dC+Z58DAWoVGzR/JiKTeMirwpwG7zbllcSw==", - "dependencies": { - "request": "^2.88.0", - "xml2js": "0.4.4" - }, - "engines": { - "node": ">=0.10.20" - } - }, - "node_modules/amazon-payments/node_modules/xml2js": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", - "integrity": "sha512-9ERdxLOo4EazMDHAS/vsuZiTXIMur6ydcRfzGrFVJ4qM78zD3ohUgPJC7NYpGwd5rnS0ufSydMJClh6jyH+V0w==", - "dependencies": { - "sax": "0.6.x", - "xmlbuilder": ">=1.0.0" - } - }, "node_modules/amplitude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/amplitude/-/amplitude-6.0.0.tgz", @@ -22731,14 +22703,6 @@ "node": ">=4.0" } }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "engines": { - "node": ">=8.0" - } - }, "node_modules/xmlcreate": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", diff --git a/package.json b/package.json index 187543936d4..a4f178e3598 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,6 @@ "@parse/node-apn": "^5.2.3", "@slack/webhook": "^6.1.0", "accepts": "^1.3.8", - "amazon": "^0.0.0", - "amazon-payments": "^0.2.9", "amplitude": "^6.0.0", "apidoc": "^0.54.0", "apple-auth": "^1.0.9", diff --git a/website/client/src/libs/payments.js b/website/client/src/libs/payments.js index f3c2a840ddf..24e84348830 100644 --- a/website/client/src/libs/payments.js +++ b/website/client/src/libs/payments.js @@ -1,8 +1,4 @@ -// import getStore from '@/store'; - export function setup () { // eslint-disable-line import/prefer-default-export - // const store = getStore(); - // Load the payment scripts // Stripe From 7a8c19972166888f00f393633104a041e114a553 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Mon, 16 Dec 2024 14:34:37 -0500 Subject: [PATCH 22/24] refactor: Add back in cancellation/add group member functionality --- package-lock.json | 30 ++ package.json | 1 + .../unit/libs/payments/amazon/cancel.test.js | 180 +++++++++++ .../payments/amazon/upgrade-groupplan.test.js | 86 +++++ .../group-plans/group-payments-cancel.test.js | 2 + test/api/unit/libs/payments/payments.test.js | 2 + ...T-payments_amazon_subscribe_cancel.test.js | 78 +++++ ...-payments_amazon_verifyAccessToken.test.js | 20 ++ test/helpers/api-integration/requester.js | 1 + website/client/src/assets/svg/amazonpay.svg | 73 +++++ .../src/components/payments/amazonModal.vue | 295 ++++++++++++++++++ .../components/payments/buttons/amazon.vue | 135 ++++++++ website/client/vue.config.js | 7 + .../controllers/top-level/payments/amazon.js | 115 +++++++ website/server/libs/payments/amazon.js | 274 ++++++++++++++++ website/server/libs/payments/subscriptions.js | 4 +- website/server/models/group.js | 8 +- website/server/models/subscriptionPlan.js | 6 +- website/server/models/user/methods.js | 6 +- 19 files changed, 1317 insertions(+), 6 deletions(-) create mode 100644 test/api/unit/libs/payments/amazon/cancel.test.js create mode 100644 test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js create mode 100644 test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js create mode 100644 test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js create mode 100644 website/client/src/assets/svg/amazonpay.svg create mode 100644 website/client/src/components/payments/amazonModal.vue create mode 100644 website/client/src/components/payments/buttons/amazon.vue create mode 100644 website/server/controllers/top-level/payments/amazon.js create mode 100644 website/server/libs/payments/amazon.js diff --git a/package-lock.json b/package-lock.json index d70696cccde..d18448792be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@parse/node-apn": "^5.2.3", "@slack/webhook": "^6.1.0", "accepts": "^1.3.8", + "amazon-payments": "^0.2.9", "amplitude": "^6.0.0", "apidoc": "^0.54.0", "apple-auth": "^1.0.9", @@ -3989,6 +3990,27 @@ "ajv": "^6.9.1" } }, + "node_modules/amazon-payments": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/amazon-payments/-/amazon-payments-0.2.9.tgz", + "integrity": "sha512-Vy5o/maYVCLSGvrPlUvoCunVffSHawN8zCJSQZcYPfqnDsLvQl8dC+Z58DAWoVGzR/JiKTeMirwpwG7zbllcSw==", + "dependencies": { + "request": "^2.88.0", + "xml2js": "0.4.4" + }, + "engines": { + "node": ">=0.10.20" + } + }, + "node_modules/amazon-payments/node_modules/xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha512-9ERdxLOo4EazMDHAS/vsuZiTXIMur6ydcRfzGrFVJ4qM78zD3ohUgPJC7NYpGwd5rnS0ufSydMJClh6jyH+V0w==", + "dependencies": { + "sax": "0.6.x", + "xmlbuilder": ">=1.0.0" + } + }, "node_modules/amplitude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/amplitude/-/amplitude-6.0.0.tgz", @@ -22703,6 +22725,14 @@ "node": ">=4.0" } }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "engines": { + "node": ">=8.0" + } + }, "node_modules/xmlcreate": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", diff --git a/package.json b/package.json index a4f178e3598..efd399bb421 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@parse/node-apn": "^5.2.3", "@slack/webhook": "^6.1.0", "accepts": "^1.3.8", + "amazon-payments": "^0.2.9", "amplitude": "^6.0.0", "apidoc": "^0.54.0", "apple-auth": "^1.0.9", diff --git a/test/api/unit/libs/payments/amazon/cancel.test.js b/test/api/unit/libs/payments/amazon/cancel.test.js new file mode 100644 index 00000000000..1d47d22a2ef --- /dev/null +++ b/test/api/unit/libs/payments/amazon/cancel.test.js @@ -0,0 +1,180 @@ +import moment from 'moment'; + +import { + generateGroup, +} from '../../../../../helpers/api-unit.helper'; +import { model as User } from '../../../../../../website/server/models/user'; +import amzLib from '../../../../../../website/server/libs/payments/amazon'; +import payments from '../../../../../../website/server/libs/payments/payments'; +import common from '../../../../../../website/common'; +import { createNonLeaderGroupMember } from '../paymentHelpers'; + +const { i18n } = common; + +describe('Amazon Payments - Cancel Subscription', () => { + const subKey = 'basic_3mo'; + + let user; let group; let headers; let billingAgreementId; let subscriptionBlock; let + subscriptionLength; + let getBillingAgreementDetailsSpy; + let paymentCancelSubscriptionSpy; + + function expectAmazonStubs () { + expect(getBillingAgreementDetailsSpy).to.be.calledOnce; + expect(getBillingAgreementDetailsSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + } + + function expectAmazonCancelSubscriptionSpy (groupId, lastBillingDate) { + expect(paymentCancelSubscriptionSpy).to.be.calledWith({ + user, + groupId, + nextBill: moment(lastBillingDate).add({ days: subscriptionLength }), + paymentMethod: amzLib.constants.PAYMENT_METHOD, + headers, + cancellationReason: undefined, + }); + } + + function expectAmazonCancelUserSubscriptionSpy () { + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expectAmazonCancelSubscriptionSpy(undefined, user.purchased.plan.lastBillingDate); + } + + function expectAmazonCancelGroupSubscriptionSpy (groupId) { + expect(paymentCancelSubscriptionSpy).to.be.calledOnce; + expectAmazonCancelSubscriptionSpy(groupId, group.purchased.plan.lastBillingDate); + } + + function expectBillingAggreementDetailSpy () { + getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails') + .resolves({ + BillingAgreementDetails: { + BillingAgreementStatus: { State: 'Open' }, + }, + }); + } + + beforeEach(async () => { + user = new User(); + user.profile.name = 'sender'; + user.purchased.plan.customerId = 'customer-id'; + user.purchased.plan.planId = subKey; + user.purchased.plan.lastBillingDate = new Date(); + + group = generateGroup({ + name: 'test group', + type: 'guild', + privacy: 'public', + leader: user._id, + }); + group.purchased.plan.customerId = 'customer-id'; + group.purchased.plan.planId = subKey; + group.purchased.plan.lastBillingDate = new Date(); + await group.save(); + + subscriptionBlock = common.content.subscriptionBlocks[subKey]; + subscriptionLength = subscriptionBlock.months * 30; + + headers = {}; + + getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails'); + getBillingAgreementDetailsSpy.resolves({ + BillingAgreementDetails: { + BillingAgreementStatus: { State: 'Closed' }, + }, + }); + + paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription'); + paymentCancelSubscriptionSpy.resolves({}); + }); + + afterEach(() => { + amzLib.getBillingAgreementDetails.restore(); + payments.cancelSubscription.restore(); + }); + + it('should throw an error if we are missing a subscription', async () => { + user.purchased.plan.customerId = undefined; + + await expect(amzLib.cancelSubscription({ user })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('missingSubscription'), + }); + }); + + it('should cancel a user subscription', async () => { + billingAgreementId = user.purchased.plan.customerId; + + await amzLib.cancelSubscription({ user, headers }); + + expectAmazonCancelUserSubscriptionSpy(); + expectAmazonStubs(); + }); + + it('should close a user subscription if amazon not closed', async () => { + amzLib.getBillingAgreementDetails.restore(); + expectBillingAggreementDetailSpy(); + const closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); + billingAgreementId = user.purchased.plan.customerId; + + await amzLib.cancelSubscription({ user, headers }); + + expectAmazonStubs(); + expect(closeBillingAgreementSpy).to.be.calledOnce; + expect(closeBillingAgreementSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + expectAmazonCancelUserSubscriptionSpy(); + amzLib.closeBillingAgreement.restore(); + }); + + it('should throw an error if group is not found', async () => { + await expect(amzLib.cancelSubscription({ user, groupId: 'fake-id' })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 404, + name: 'NotFound', + message: i18n.t('groupNotFound'), + }); + }); + + it('should throw an error if user is not group leader', async () => { + const nonLeader = await createNonLeaderGroupMember(group); + + await expect(amzLib.cancelSubscription({ user: nonLeader, groupId: group._id })) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: i18n.t('onlyGroupLeaderCanManageSubscription'), + }); + }); + + it('should cancel a group subscription', async () => { + billingAgreementId = group.purchased.plan.customerId; + + await amzLib.cancelSubscription({ user, groupId: group._id, headers }); + + expectAmazonCancelGroupSubscriptionSpy(group._id); + expectAmazonStubs(); + }); + + it('should close a group subscription if amazon not closed', async () => { + amzLib.getBillingAgreementDetails.restore(); + expectBillingAggreementDetailSpy(); + const closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); + billingAgreementId = group.purchased.plan.customerId; + + await amzLib.cancelSubscription({ user, groupId: group._id, headers }); + + expectAmazonStubs(); + expect(closeBillingAgreementSpy).to.be.calledOnce; + expect(closeBillingAgreementSpy).to.be.calledWith({ + AmazonBillingAgreementId: billingAgreementId, + }); + expectAmazonCancelGroupSubscriptionSpy(group._id); + amzLib.closeBillingAgreement.restore(); + }); +}); \ No newline at end of file diff --git a/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js b/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js new file mode 100644 index 00000000000..b37fdde0310 --- /dev/null +++ b/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js @@ -0,0 +1,86 @@ +import { + generateGroup, +} from '../../../../../helpers/api-unit.helper'; +import { model as User } from '../../../../../../website/server/models/user'; +import { model as Group } from '../../../../../../website/server/models/group'; +import amzLib from '../../../../../../website/server/libs/payments/amazon'; +import payments from '../../../../../../website/server/libs/payments/payments'; +import common from '../../../../../../website/common'; + +describe('#upgradeGroupPlan', () => { + let spy; let data; let user; let group; let + uuidString; + + beforeEach(async () => { + user = new User(); + user.profile.name = 'sender'; + + data = { + user, + sub: { + key: 'basic_3mo', // @TODO: Validate that this is group + }, + customerId: 'customer-id', + paymentMethod: 'Payment Method', + headers: { + 'x-client': 'habitica-web', + 'user-agent': '', + }, + }; + + group = generateGroup({ + name: 'test group', + type: 'guild', + privacy: 'private', + leader: user._id, + }); + await group.save(); + + user.guilds.push(group._id); + await user.save(); + + spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement'); + spy.resolves([]); + + uuidString = 'uuid-v4'; + sinon.stub(common, 'uuid').returns(uuidString); + + data.groupId = group._id; + data.sub.quantity = 3; + }); + + afterEach(() => { + amzLib.authorizeOnBillingAgreement.restore(); + common.uuid.restore(); + }); + + it('charges for a new member', async () => { + data.paymentMethod = amzLib.constants.PAYMENT_METHOD; + await payments.createSubscription(data); + + const updatedGroup = await Group.findById(group._id).exec(); + + updatedGroup.memberCount += 1; + await updatedGroup.save(); + + await amzLib.chargeForAdditionalGroupMember(updatedGroup); + + expect(spy.calledOnce).to.be.true; + expect(spy).to.be.calledWith({ + AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId, + AuthorizationReferenceId: uuidString.substring(0, 32), + AuthorizationAmount: { + CurrencyCode: amzLib.constants.CURRENCY_CODE, + Amount: 3, + }, + SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER, + TransactionTimeout: 0, + CaptureNow: true, + SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER, + SellerOrderAttributes: { + SellerOrderId: uuidString, + StoreName: amzLib.constants.STORE_NAME, + }, + }); + }); +}); \ No newline at end of file diff --git a/test/api/unit/libs/payments/group-plans/group-payments-cancel.test.js b/test/api/unit/libs/payments/group-plans/group-payments-cancel.test.js index 0dc192fb633..49cb80d4e3a 100644 --- a/test/api/unit/libs/payments/group-plans/group-payments-cancel.test.js +++ b/test/api/unit/libs/payments/group-plans/group-payments-cancel.test.js @@ -290,6 +290,7 @@ describe('Canceling a subscription for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); + expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated); }); @@ -328,6 +329,7 @@ describe('Canceling a subscription for group', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond); expect(updatedUser.purchased.plan.dateTerminated).to.exist; + expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated); }); }); diff --git a/test/api/unit/libs/payments/payments.test.js b/test/api/unit/libs/payments/payments.test.js index 23aa8a18ddd..be5c4e6c5a8 100644 --- a/test/api/unit/libs/payments/payments.test.js +++ b/test/api/unit/libs/payments/payments.test.js @@ -62,6 +62,7 @@ describe('payments/index', () => { extraMonths: 0, dateTerminated: null, dateCreated: new Date(), + lastBillingDate: new Date(), mysteryItems: [], consecutive: { trinkets: 0, @@ -1381,6 +1382,7 @@ describe('payments/index', () => { expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan'); expect(updatedUser.purchased.plan.extraMonths).to.eql(0); expect(updatedUser.purchased.plan.dateTerminated).to.eql(null); + expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist; expect(updatedUser.purchased.plan.dateCreated).to.exist; }); diff --git a/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js new file mode 100644 index 00000000000..7c2466f0f2c --- /dev/null +++ b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js @@ -0,0 +1,78 @@ +import { + createAndPopulateGroup, + generateUser, + translate as t, +} from '../../../../../helpers/api-integration/v3'; +import amzLib from '../../../../../../website/server/libs/payments/amazon'; + +describe('payments : amazon #subscribeCancel', () => { + const endpoint = '/amazon/subscribe/cancel?noRedirect=true'; + let user; let group; let + amazonSubscribeCancelStub; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('throws error when there users has no subscription', async () => { + await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('missingSubscription'), + }); + }); + + describe('success', () => { + beforeEach(() => { + amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').resolves({}); + }); + + afterEach(() => { + amzLib.cancelSubscription.restore(); + }); + + it('cancels a user subscription', async () => { + user = await generateUser({ + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }); + + await user.get(endpoint); + + expect(amazonSubscribeCancelStub).to.be.calledOnce; + expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); + expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(undefined); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + + it('cancels a group subscription', async () => { + ({ group, groupLeader: user } = await createAndPopulateGroup({ + groupDetails: { + name: 'test group', + type: 'guild', + privacy: 'private', + }, + leaderDetails: { + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }, + upgradeToGroupPlan: true, + })); + + await user.get(`${endpoint}&groupId=${group._id}`); + + expect(amazonSubscribeCancelStub).to.be.calledOnce; + expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id); + expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(group._id); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken); + expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id); + }); + }); +}); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js new file mode 100644 index 00000000000..e7e0b81d655 --- /dev/null +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_verifyAccessToken.test.js @@ -0,0 +1,20 @@ +import { + generateUser, +} from '../../../../../helpers/api-integration/v3'; + +describe('payments : amazon', () => { + const endpoint = '/amazon/verifyAccessToken'; + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('verifies access token', async () => { + await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'Missing req.body.access_token', + }); + }); +}); diff --git a/test/helpers/api-integration/requester.js b/test/helpers/api-integration/requester.js index 881f0b9ec5f..61c0b2fabf2 100644 --- a/test/helpers/api-integration/requester.js +++ b/test/helpers/api-integration/requester.js @@ -38,6 +38,7 @@ function _requestMaker (user, method, additionalSets = {}) { route.indexOf('/email') === 0 || route.indexOf('/export') === 0 || route.indexOf('/paypal') === 0 + || route.indexOf('/amazon') === 0 || route.indexOf('/stripe') === 0 || route.indexOf('/analytics') === 0 ) { diff --git a/website/client/src/assets/svg/amazonpay.svg b/website/client/src/assets/svg/amazonpay.svg new file mode 100644 index 00000000000..76bd1f66c44 --- /dev/null +++ b/website/client/src/assets/svg/amazonpay.svg @@ -0,0 +1,73 @@ + + + + +Zeichenfläche 1 + + + + + + + + + + + + + + diff --git a/website/client/src/components/payments/amazonModal.vue b/website/client/src/components/payments/amazonModal.vue new file mode 100644 index 00000000000..fa332be1499 --- /dev/null +++ b/website/client/src/components/payments/amazonModal.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/website/client/src/components/payments/buttons/amazon.vue b/website/client/src/components/payments/buttons/amazon.vue new file mode 100644 index 00000000000..7c58e782140 --- /dev/null +++ b/website/client/src/components/payments/buttons/amazon.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/website/client/vue.config.js b/website/client/vue.config.js index 3783d0cedc3..e34e6d0c018 100644 --- a/website/client/vue.config.js +++ b/website/client/vue.config.js @@ -15,6 +15,9 @@ setupNconf(configFile, nconf); const DEV_BASE_URL = nconf.get('BASE_URL'); const envVars = [ + 'AMAZON_PAYMENTS_SELLER_ID', + 'AMAZON_PAYMENTS_CLIENT_ID', + 'AMAZON_PAYMENTS_MODE', 'EMAILS_COMMUNITY_MANAGER_EMAIL', 'EMAILS_TECH_ASSISTANCE_EMAIL', 'EMAILS_PRESS_ENQUIRY_EMAIL', @@ -171,6 +174,10 @@ module.exports = { target: DEV_BASE_URL, changeOrigin: true, }, + '^/amazon': { + target: DEV_BASE_URL, + changeOrigin: true, + }, '^/paypal': { target: DEV_BASE_URL, changeOrigin: true, diff --git a/website/server/controllers/top-level/payments/amazon.js b/website/server/controllers/top-level/payments/amazon.js new file mode 100644 index 00000000000..6f7b78db77c --- /dev/null +++ b/website/server/controllers/top-level/payments/amazon.js @@ -0,0 +1,115 @@ +import { + BadRequest, +} from '../../../libs/errors'; +import amzLib from '../../../libs/payments/amazon'; +import { + authWithHeaders, +} from '../../../middlewares/auth'; + +const api = {}; + +/** + * @apiIgnore Payments are considered part of the private API + * @api {post} /amazon/verifyAccessToken Amazon Payments: verify access token + * @apiName AmazonVerifyAccessToken + * @apiGroup Payments + * + * @apiSuccess {Object} data Empty object + * */ +api.verifyAccessToken = { + method: 'POST', + url: '/amazon/verifyAccessToken', + middlewares: [authWithHeaders()], + async handler (req, res) { + const accessToken = req.body.access_token; + + if (!accessToken) throw new BadRequest('Missing req.body.access_token'); + + await amzLib.getTokenInfo(accessToken); + + res.respond(200, {}); + }, +}; + +/** + * @apiIgnore Payments are considered part of the private API + * @api {post} /amazon/createOrderReferenceId Amazon Payments: create order reference id + * @apiName AmazonCreateOrderReferenceId + * @apiGroup Payments + * + * @apiSuccess {String} data.orderReferenceId The order reference id. + * */ +api.createOrderReferenceId = { + method: 'POST', + url: '/amazon/createOrderReferenceId', + middlewares: [authWithHeaders()], + async handler (req, res) { + const { billingAgreementId } = req.body; + + if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId'); + + const response = await amzLib.createOrderReferenceId({ + Id: billingAgreementId, + IdType: 'BillingAgreement', + ConfirmNow: false, + }); + + res.respond(200, { + orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId, + }); + }, +}; + +/** + * @apiIgnore Payments are considered part of the private API + * @api {post} /amazon/checkout Amazon Payments: checkout + * @apiName AmazonCheckout + * @apiGroup Payments + * + * @apiSuccess {Object} data Empty object + * */ +api.checkout = { + method: 'POST', + url: '/amazon/checkout', + middlewares: [authWithHeaders()], + async handler (req, res) { + const { user } = res.locals; + const { + orderReferenceId, gift, gemsBlock, sku, + } = req.body; + + if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId'); + + await amzLib.checkout({ + gemsBlock, gift, sku, user, orderReferenceId, headers: req.headers, + }); + + res.respond(200); + }, +}; + +/** + * @apiIgnore Payments are considered part of the private API + * @api {get} /amazon/subscribe/cancel Amazon Payments: subscribe cancel + * @apiName AmazonSubscribe + * @apiGroup Payments + * */ +api.subscribeCancel = { + method: 'GET', + url: '/amazon/subscribe/cancel', + middlewares: [authWithHeaders()], + async handler (req, res) { + const { user } = res.locals; + const { groupId } = req.query; + + await amzLib.cancelSubscription({ user, groupId, headers: req.headers }); + + if (req.query.noRedirect) { + res.respond(200); + } else { + res.redirect('/'); + } + }, +}; + +export default api; diff --git a/website/server/libs/payments/amazon.js b/website/server/libs/payments/amazon.js new file mode 100644 index 00000000000..d86986bb2e4 --- /dev/null +++ b/website/server/libs/payments/amazon.js @@ -0,0 +1,274 @@ +import amazonPayments from 'amazon-payments'; +import nconf from 'nconf'; +import moment from 'moment'; +import cc from 'coupon-code'; +import util from 'util'; + +import common from '../../../common'; +import { + BadRequest, + NotAuthorized, + NotFound, +} from '../errors'; +import payments from './payments'; // eslint-disable-line import/no-cycle +import { // eslint-disable-line import/no-cycle + model as Group, + basicFields as basicGroupFields, +} from '../../models/group'; +import { model as Coupon } from '../../models/coupon'; + +// TODO better handling of errors + +const { i18n } = common; +const IS_SANDBOX = nconf.get('AMAZON_PAYMENTS_MODE') === 'sandbox'; + +const amzPayment = amazonPayments.connect({ + environment: amazonPayments.Environment[IS_SANDBOX ? 'Sandbox' : 'Production'], + sellerId: nconf.get('AMAZON_PAYMENTS_SELLER_ID'), + mwsAccessKey: nconf.get('AMAZON_PAYMENTS_MWS_KEY'), + mwsSecretKey: nconf.get('AMAZON_PAYMENTS_MWS_SECRET'), + clientId: nconf.get('AMAZON_PAYMENTS_CLIENT_ID'), +}); + +const api = {}; + +api.constants = { + CURRENCY_CODE: 'USD', + SELLER_NOTE: 'Habitica Payment', + SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment', + SELLER_NOTE_GROUP_NEW_MEMBER: 'Habitica Group Plan New Member', + STORE_NAME: 'Habitica', +}; + +api.getTokenInfo = util.promisify(amzPayment.api.getTokenInfo).bind(amzPayment.api); +api.createOrderReferenceId = util + .promisify(amzPayment.offAmazonPayments.createOrderReferenceForId) + .bind(amzPayment.offAmazonPayments); +api.setOrderReferenceDetails = util + .promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails) + .bind(amzPayment.offAmazonPayments); +api.confirmOrderReference = util + .promisify(amzPayment.offAmazonPayments.confirmOrderReference) + .bind(amzPayment.offAmazonPayments); +api.closeOrderReference = util + .promisify(amzPayment.offAmazonPayments.closeOrderReference) + .bind(amzPayment.offAmazonPayments); +api.setBillingAgreementDetails = util + .promisify(amzPayment.offAmazonPayments.setBillingAgreementDetails) + .bind(amzPayment.offAmazonPayments); +api.getBillingAgreementDetails = util + .promisify(amzPayment.offAmazonPayments.getBillingAgreementDetails) + .bind(amzPayment.offAmazonPayments); +api.confirmBillingAgreement = util + .promisify(amzPayment.offAmazonPayments.confirmBillingAgreement) + .bind(amzPayment.offAmazonPayments); +api.closeBillingAgreement = util + .promisify(amzPayment.offAmazonPayments.closeBillingAgreement) + .bind(amzPayment.offAmazonPayments); + +api.authorizeOnBillingAgreement = function authorizeOnBillingAgreement (inputSet) { + return new Promise((resolve, reject) => { + amzPayment.offAmazonPayments.authorizeOnBillingAgreement(inputSet, (err, response) => { + if (err) return reject(err); + if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(new BadRequest(i18n.t('paymentNotSuccessful'))); + return resolve(response); + }); + }); +}; + +api.authorize = function authorize (inputSet) { + return new Promise((resolve, reject) => { + amzPayment.offAmazonPayments.authorize(inputSet, (err, response) => { + if (err) return reject(err); + if (response.AuthorizationDetails.AuthorizationStatus.State === 'Declined') return reject(new BadRequest(i18n.t('paymentNotSuccessful'))); + return resolve(response); + }); + }); +}; + +/** + * Cancel an Amazon Subscription + * + * @param options + * @param options.user The user object who is canceling + * @param options.groupId The id of the group that is canceling + * @param options.headers The request headers + * @param options.cancellationReason A text string to control sending an email + * + * @return undefined + */ +api.cancelSubscription = async function cancelSubscription (options = {}) { + const { + user, groupId, headers, cancellationReason, + } = options; + + let billingAgreementId; + let planId; + let lastBillingDate; + + if (groupId) { + const groupFields = basicGroupFields.concat(' purchased'); + const group = await Group.getGroup({ + user, groupId, populateLeader: false, groupFields, + }); + + if (!group) { + throw new NotFound(i18n.t('groupNotFound')); + } + + if (group.leader !== user._id) { + throw new NotAuthorized(i18n.t('onlyGroupLeaderCanManageSubscription')); + } + + billingAgreementId = group.purchased.plan.customerId; + planId = group.purchased.plan.planId; + lastBillingDate = group.purchased.plan.lastBillingDate; + } else { + billingAgreementId = user.purchased.plan.customerId; + planId = user.purchased.plan.planId; + lastBillingDate = user.purchased.plan.lastBillingDate; + } + + if (!billingAgreementId) throw new NotAuthorized(i18n.t('missingSubscription')); + + const details = await this.getBillingAgreementDetails({ + AmazonBillingAgreementId: billingAgreementId, + }).catch(err => err); + + const badBAStates = ['Canceled', 'Closed', 'Suspended']; + if ( + details + && details.BillingAgreementDetails + && details.BillingAgreementDetails.BillingAgreementStatus + && badBAStates.indexOf(details.BillingAgreementDetails.BillingAgreementStatus.State) === -1 + ) { + await this.closeBillingAgreement({ + AmazonBillingAgreementId: billingAgreementId, + }); + } + + const subscriptionBlock = common.content.subscriptionBlocks[planId]; + const subscriptionLength = subscriptionBlock.months * 30; + + await payments.cancelSubscription({ + user, + groupId, + nextBill: moment(lastBillingDate).add({ days: subscriptionLength }), + paymentMethod: this.constants.PAYMENT_METHOD, + headers, + cancellationReason, + }); +}; + +/** + * Allows for purchasing a user subscription or group subscription with Amazon + * + * @param options + * @param options.billingAgreementId The Amazon billingAgreementId generated on the front end + * @param options.user The user object who is purchasing + * @param options.sub The subscription data to purchase + * @param options.coupon The coupon to discount the sub + * @param options.groupId The id of the group purchasing a subscription + * @param options.headers The request headers to store on analytics + * @return undefined + */ +api.subscribe = async function subscribe (options) { + const { + billingAgreementId, + sub, + coupon, + user, + groupId, + headers, + } = options; + + if (!sub) throw new BadRequest(i18n.t('missingSubscriptionCode')); + if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId'); + + if (sub.discount) { // apply discount + if (!coupon) throw new BadRequest(i18n.t('couponCodeRequired')); + const result = await Coupon.findOne({ _id: cc.validate(coupon), event: sub.key }).exec(); + if (!result) throw new NotAuthorized(i18n.t('invalidCoupon')); + } + + let amount = sub.price; + const leaderCount = 1; + const priceOfSingleMember = 3; + + if (groupId) { + const groupFields = basicGroupFields.concat(' purchased'); + const group = await Group.getGroup({ + user, groupId, populateLeader: false, groupFields, + }); + const membersCount = await group.getMemberCount(); + amount = sub.price + (membersCount - leaderCount) * priceOfSingleMember; + } + + await this.setBillingAgreementDetails({ + AmazonBillingAgreementId: billingAgreementId, + BillingAgreementAttributes: { + SellerNote: this.constants.SELLER_NOTE_SUBSCRIPTION, + SellerBillingAgreementAttributes: { + SellerBillingAgreementId: common.uuid(), + StoreName: this.constants.STORE_NAME, + CustomInformation: this.constants.SELLER_NOTE_SUBSCRIPTION, + }, + }, + }); + + await this.confirmBillingAgreement({ + AmazonBillingAgreementId: billingAgreementId, + }); + + await this.authorizeOnBillingAgreement({ + AmazonBillingAgreementId: billingAgreementId, + AuthorizationReferenceId: common.uuid().substring(0, 32), + AuthorizationAmount: { + CurrencyCode: this.constants.CURRENCY_CODE, + Amount: amount, + }, + SellerAuthorizationNote: this.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, + TransactionTimeout: 0, + CaptureNow: true, + SellerNote: this.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION, + SellerOrderAttributes: { + SellerOrderId: common.uuid(), + StoreName: this.constants.STORE_NAME, + }, + }); + + await payments.createSubscription({ + user, + customerId: billingAgreementId, + paymentMethod: this.constants.PAYMENT_METHOD, + sub, + headers, + groupId, + }); +}; + +api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) { + // @TODO: Can we get this from the content plan? + const priceForNewMember = 3; + + // @TODO: Prorate? + + return this.authorizeOnBillingAgreement({ + AmazonBillingAgreementId: group.purchased.plan.customerId, + AuthorizationReferenceId: common.uuid().substring(0, 32), + AuthorizationAmount: { + CurrencyCode: this.constants.CURRENCY_CODE, + Amount: priceForNewMember, + }, + SellerAuthorizationNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER, + TransactionTimeout: 0, + CaptureNow: true, + SellerNote: this.constants.SELLER_NOTE_GROUP_NEW_MEMBER, + SellerOrderAttributes: { + SellerOrderId: common.uuid(), + StoreName: this.constants.STORE_NAME, + }, + }); +}; + +export default api; diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index 06608fd64b4..4936e2b19ef 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -190,7 +190,9 @@ async function prepareSubscriptionValues (data) { paymentMethod: data.paymentMethod, extraMonths: Number(plan.extraMonths) + _dateDiff(today, plan.dateTerminated), dateTerminated: null, - lastBillingDate: today, + // Specify a lastBillingDate just for Amazon Payments + // Resetted every time the subscription restarts + lastBillingDate: data.paymentMethod === 'Amazon Payments' ? today : undefined, nextPaymentProcessing: data.nextPaymentProcessing, nextBillingDate: data.nextBillingDate, additionalData: data.additionalData, diff --git a/website/server/models/group.js b/website/server/models/group.js index db50cd90115..6db5ff3e056 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -33,6 +33,7 @@ import { schema as SubscriptionPlanSchema, } from './subscriptionPlan'; import logger from '../libs/logger'; +import amazonPayments from '../libs/payments/amazon'; // eslint-disable-line import/no-cycle import stripePayments from '../libs/payments/stripe'; // eslint-disable-line import/no-cycle import { getGroupChat, translateMessage } from '../libs/chat/group-chat'; // eslint-disable-line import/no-cycle import { model as UserNotification } from './userNotification'; @@ -1621,12 +1622,17 @@ schema.methods.hasCancelled = function hasCancelled () { return Boolean(this.hasActiveGroupPlan() && plan.dateTerminated); }; -schema.methods.updateGroupPlan = async function updateGroupPlan () { +schema.methods.updateGroupPlan = async function updateGroupPlan (removingMember) { // Recheck the group plan count this.memberCount = await this.getMemberCount(); if (this.purchased.plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) { await stripePayments.chargeForAdditionalGroupMember(this); + } else if ( + this.purchased.plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD + && !removingMember + ) { + await amazonPayments.chargeForAdditionalGroupMember(this); } }; diff --git a/website/server/models/subscriptionPlan.js b/website/server/models/subscriptionPlan.js index 067d4063abf..c1b534ab55e 100644 --- a/website/server/models/subscriptionPlan.js +++ b/website/server/models/subscriptionPlan.js @@ -8,8 +8,8 @@ export const schema = new mongoose.Schema({ subscriptionId: String, owner: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for subscription owner.'] }, quantity: { $type: Number, default: 1 }, - paymentMethod: String, // enum: ['Paypal', 'Stripe', 'Gift', 'Google', '']} - customerId: String, + paymentMethod: String, // enum: ['Paypal', 'Stripe', 'Gift', 'Amazon Payments', Google', '']} + customerId: String, // Billing Agreement Id in case of Amazon Payments dateCreated: Date, dateTerminated: Date, dateUpdated: Date, @@ -18,7 +18,7 @@ export const schema = new mongoose.Schema({ gemsBought: { $type: Number, default: 0 }, mysteryItems: { $type: Array, default: () => [] }, lastReminderDate: Date, // indicates the last time a subscription reminder was sent - lastBillingDate: Date, + lastBillingDate: Date, // Used only for Amazon Payments to keep track of billing date // Example for Google: {'receipt': 'serialized receipt json', 'signature': 'signature string'} additionalData: mongoose.Schema.Types.Mixed, // indicates when the queue server should process this subscription again. diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index 847292afd0e..59ba50c41f7 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -19,6 +19,7 @@ import { model as UserNotification } from '../userNotification'; import schema from './schema'; // eslint-disable-line import/no-cycle import payments from '../../libs/payments/payments'; // eslint-disable-line import/no-cycle import * as inboxLib from '../../libs/inbox'; // eslint-disable-line import/no-cycle +import amazonPayments from '../../libs/payments/amazon'; // eslint-disable-line import/no-cycle import stripePayments from '../../libs/payments/stripe'; // eslint-disable-line import/no-cycle import paypalPayments from '../../libs/payments/paypal'; // eslint-disable-line import/no-cycle import { model as NewsPost } from '../newsPost'; @@ -325,6 +326,7 @@ schema.statics.addComputedStatsToJSONObj = function addComputedStatsToUserJSONOb * @param options * @param options.user The user object who is purchasing * @param options.groupId The id of the group purchasing a subscription + * @param options.headers The request headers (only for Amazon subscriptions) * @param options.cancellationReason A text string to control sending an email * * @return a Promise from api.cancelSubscription() @@ -342,7 +344,9 @@ schema.methods.cancelSubscription = async function cancelSubscription (options = const { plan } = this.purchased; options.user = this; - if (plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) { + if (plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD) { + return amazonPayments.cancelSubscription(options); + } if (plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) { return stripePayments.cancelSubscription(options); } if (plan.paymentMethod === paypalPayments.constants.PAYMENT_METHOD) { return paypalPayments.subscribeCancel(options); From 2e4457d1c9ff4d1edc7c1cd134266d4273174d67 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Mon, 16 Dec 2024 15:19:58 -0500 Subject: [PATCH 23/24] chore: update habitica-images --- habitica-images | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/habitica-images b/habitica-images index 47909bcc2d1..dedbcf0f24e 160000 --- a/habitica-images +++ b/habitica-images @@ -1 +1 @@ -Subproject commit 47909bcc2d12a126fe59d4046432a7762a3fa2c3 +Subproject commit dedbcf0f24e0147155c0782b3f3b3195f84b297e From 4b49938b6fc0a4aff350f296533999e3e5e21426 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Tue, 17 Dec 2024 15:54:51 -0500 Subject: [PATCH 24/24] refactor: updates to Amazon tests & subscriptionPlan.js --- test/api/unit/libs/payments/amazon/cancel.test.js | 2 +- test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js | 2 +- website/client/src/mixins/payments.js | 2 +- website/server/libs/payments/subscriptions.js | 2 +- website/server/models/subscriptionPlan.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/api/unit/libs/payments/amazon/cancel.test.js b/test/api/unit/libs/payments/amazon/cancel.test.js index 1d47d22a2ef..e24d3dbf522 100644 --- a/test/api/unit/libs/payments/amazon/cancel.test.js +++ b/test/api/unit/libs/payments/amazon/cancel.test.js @@ -177,4 +177,4 @@ describe('Amazon Payments - Cancel Subscription', () => { expectAmazonCancelGroupSubscriptionSpy(group._id); amzLib.closeBillingAgreement.restore(); }); -}); \ No newline at end of file +}); diff --git a/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js b/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js index b37fdde0310..26cec748355 100644 --- a/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js +++ b/test/api/unit/libs/payments/amazon/upgrade-groupplan.test.js @@ -83,4 +83,4 @@ describe('#upgradeGroupPlan', () => { }, }); }); -}); \ No newline at end of file +}); diff --git a/website/client/src/mixins/payments.js b/website/client/src/mixins/payments.js index b8d724f313e..17ce0a785c6 100644 --- a/website/client/src/mixins/payments.js +++ b/website/client/src/mixins/payments.js @@ -279,7 +279,7 @@ export default { let paymentMethod = group ? group.purchased.plan.paymentMethod : this.user.purchased.plan.paymentMethod; - paymentMethod = paymentMethod.toLowerCase(); + paymentMethod = paymentMethod === 'Amazon Payments' ? 'amazon' : paymentMethod.toLowerCase(); const queryParams = { noRedirect: true, diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index 4936e2b19ef..0274a31e797 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -192,7 +192,7 @@ async function prepareSubscriptionValues (data) { dateTerminated: null, // Specify a lastBillingDate just for Amazon Payments // Resetted every time the subscription restarts - lastBillingDate: data.paymentMethod === 'Amazon Payments' ? today : undefined, + // lastBillingDate: data.paymentMethod === 'Amazon Payments' ? today : undefined, nextPaymentProcessing: data.nextPaymentProcessing, nextBillingDate: data.nextBillingDate, additionalData: data.additionalData, diff --git a/website/server/models/subscriptionPlan.js b/website/server/models/subscriptionPlan.js index c1b534ab55e..4943705e898 100644 --- a/website/server/models/subscriptionPlan.js +++ b/website/server/models/subscriptionPlan.js @@ -8,7 +8,7 @@ export const schema = new mongoose.Schema({ subscriptionId: String, owner: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for subscription owner.'] }, quantity: { $type: Number, default: 1 }, - paymentMethod: String, // enum: ['Paypal', 'Stripe', 'Gift', 'Amazon Payments', Google', '']} + paymentMethod: String, // enum: ['Paypal', 'Stripe', 'Gift', 'Amazon Payments', 'Google', '']} customerId: String, // Billing Agreement Id in case of Amazon Payments dateCreated: Date, dateTerminated: Date,