зеркало из https://github.com/mozilla/fxa.git
feat(auth): add coupon validation
Because: * We want to know if coupons are created with product requirements as we enforce them differently. This commit: * Reports a product restriction on a coupon to Sentry if one is created in Stripe. Closes #11427
This commit is contained in:
Родитель
4733484799
Коммит
28ed143af1
|
@ -580,6 +580,13 @@ export class StripeHelper {
|
|||
});
|
||||
}
|
||||
|
||||
/** Fetch a coupon with `applies_to` expanded. */
|
||||
async getCoupon(couponId: string) {
|
||||
return this.stripe.coupons.retrieve(couponId, {
|
||||
expand: ['applies_to'],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a given promotion code is
|
||||
* a valid code in the system for the given price, and if it hasn't
|
||||
|
|
|
@ -75,6 +75,10 @@ export class StripeWebhookHandler extends StripeHandler {
|
|||
await this.handleCreditNoteEvent(request, event);
|
||||
}
|
||||
break;
|
||||
case 'coupon.created':
|
||||
case 'coupon.updated':
|
||||
await this.handleCouponEvent(request, event);
|
||||
break;
|
||||
case 'customer.created':
|
||||
// We don't need to setup the local customer if it happened via API
|
||||
// because we already set this up during creation.
|
||||
|
@ -212,6 +216,25 @@ export class StripeWebhookHandler extends StripeHandler {
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle `coupon.created` and `coupon.updated` Stripe webhook events.
|
||||
*
|
||||
* Verify that the coupon confirms to our requirements, currently that it:
|
||||
* - Does not have a product ID requirement.
|
||||
*/
|
||||
async handleCouponEvent(request: AuthRequest, event: Stripe.Event) {
|
||||
const eventCoupon = event.data.object as Stripe.Coupon;
|
||||
const coupon = await this.stripeHelper.getCoupon(eventCoupon.id);
|
||||
|
||||
if (coupon.applies_to?.products && coupon.applies_to?.products.length > 0) {
|
||||
reportSentryError(
|
||||
new Error(`Coupon has a product requirement: ${coupon.id}.`),
|
||||
request
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle `customer.created` Stripe webhook events.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"id": "evt_1GQf15BVqmGyQTMamwMk1kdP",
|
||||
"object": "event",
|
||||
"api_version": "2019-12-03",
|
||||
"created": 1585164955,
|
||||
"data": {
|
||||
"object": {
|
||||
"id": "dlCMiuwT",
|
||||
"object": "coupon",
|
||||
"amount_off": null,
|
||||
"created": 1641498279,
|
||||
"currency": null,
|
||||
"duration": "forever",
|
||||
"duration_in_months": null,
|
||||
"livemode": false,
|
||||
"max_redemptions": null,
|
||||
"metadata": {},
|
||||
"name": "Test1234",
|
||||
"percent_off": 50,
|
||||
"redeem_by": null,
|
||||
"times_redeemed": 0,
|
||||
"valid": true
|
||||
}
|
||||
},
|
||||
"livemode": false,
|
||||
"pending_webhooks": 0,
|
||||
"request": {},
|
||||
"type": "coupon.created"
|
||||
}
|
|
@ -1020,6 +1020,15 @@ describe('StripeHelper', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getCoupon', () => {
|
||||
it('returns a coupon', async () => {
|
||||
const coupon = { id: 'couponId' };
|
||||
sandbox.stub(stripeHelper.stripe.coupons, 'retrieve').resolves(coupon);
|
||||
const actual = await stripeHelper.getCoupon('couponId');
|
||||
assert.deepEqual(actual, coupon);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findValidPromoCode', () => {
|
||||
it('finds a valid promotionCode with plan metadata', async () => {
|
||||
const promotionCode = { code: 'promo1', coupon: { valid: true } };
|
||||
|
|
|
@ -31,6 +31,7 @@ const eventInvoiceCreated = require('../../payments/fixtures/stripe/event_invoic
|
|||
const eventInvoicePaid = require('../../payments/fixtures/stripe/event_invoice_paid.json');
|
||||
const eventInvoicePaidDiscount = require('../../payments/fixtures/stripe/invoice_paid_subscription_create_discount.json');
|
||||
const eventInvoicePaymentFailed = require('../../payments/fixtures/stripe/event_invoice_payment_failed.json');
|
||||
const eventCouponCreated = require('../../payments/fixtures/stripe/event_coupon_created.json');
|
||||
const eventCustomerUpdated = require('../../payments/fixtures/stripe/event_customer_updated.json');
|
||||
const eventCustomerSubscriptionUpdated = require('../../payments/fixtures/stripe/event_customer_subscription_updated.json');
|
||||
const eventCustomerSourceExpiring = require('../../payments/fixtures/stripe/event_customer_source_expiring.json');
|
||||
|
@ -161,6 +162,7 @@ describe('StripeWebhookHandler', () => {
|
|||
},
|
||||
};
|
||||
const handlerNames = [
|
||||
'handleCouponEvent',
|
||||
'handleCustomerCreatedEvent',
|
||||
'handleSubscriptionCreatedEvent',
|
||||
'handleSubscriptionUpdatedEvent',
|
||||
|
@ -257,6 +259,20 @@ describe('StripeWebhookHandler', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('when the event.type is coupon.created', () => {
|
||||
itOnlyCallsThisHandler('handleCouponEvent', {
|
||||
data: { object: { id: 'coupon_123' } },
|
||||
type: 'coupon.created',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the event.type is coupon.updated', () => {
|
||||
itOnlyCallsThisHandler('handleCouponEvent', {
|
||||
data: { object: { id: 'coupon_123' } },
|
||||
type: 'coupon.updated',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the event.type is customer.created', () => {
|
||||
itOnlyCallsThisHandler('handleCustomerCreatedEvent', {
|
||||
data: { object: customerFixture },
|
||||
|
@ -376,6 +392,34 @@ describe('StripeWebhookHandler', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('handleCouponEvents', () => {
|
||||
for (const eventType of ['coupon.created', 'coupon.updated']) {
|
||||
it(`allows a valid coupon on ${eventType}`, async () => {
|
||||
const event = deepCopy(eventCouponCreated);
|
||||
event.type = eventType;
|
||||
const coupon = deepCopy(event.data.object);
|
||||
const sentryModule = require('../../../../lib/sentry');
|
||||
coupon.applies_to = { products: [] };
|
||||
sandbox.stub(sentryModule, 'reportSentryError').returns({});
|
||||
StripeWebhookHandlerInstance.stripeHelper.getCoupon.resolves(coupon);
|
||||
await StripeWebhookHandlerInstance.handleCouponEvent({}, event);
|
||||
sinon.assert.notCalled(sentryModule.reportSentryError);
|
||||
});
|
||||
|
||||
it(`reports an error for invalid coupon on ${eventType}`, async () => {
|
||||
const event = deepCopy(eventCouponCreated);
|
||||
event.type = eventType;
|
||||
const coupon = deepCopy(event.data.object);
|
||||
const sentryModule = require('../../../../lib/sentry');
|
||||
coupon.applies_to = { products: ['productOhNo'] };
|
||||
sandbox.stub(sentryModule, 'reportSentryError').returns({});
|
||||
StripeWebhookHandlerInstance.stripeHelper.getCoupon.resolves(coupon);
|
||||
await StripeWebhookHandlerInstance.handleCouponEvent({}, event);
|
||||
sinon.assert.calledOnce(sentryModule.reportSentryError);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('handleCustomerCreatedEvent', () => {
|
||||
it('creates a local db record with the account uid', async () => {
|
||||
await StripeWebhookHandlerInstance.handleCustomerCreatedEvent(
|
||||
|
|
Загрузка…
Ссылка в новой задаче