Skip to content

Commit

Permalink
Merge pull request recurly#417 from recurly/fix-checkout-coupon-curre…
Browse files Browse the repository at this point in the history
…ncy-safety

Fix checkout coupon currency safety
  • Loading branch information
cookrn authored Dec 13, 2017
2 parents 8bb805b + a4ed077 commit 3fb70b8
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 27 deletions.
6 changes: 4 additions & 2 deletions lib/recurly/pricing/checkout/calculations.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,10 @@ export default class Calculations {
discountNext = roundForDiscount(discountableNext * coupon.discount.rate);
} else if (coupon.discount.amount) {
const { discountableNow, discountableNext } = this.discountableSubtotals(coupon);
discountNow = Math.min(discountableNow, coupon.discount.amount[this.items.currency]);
discountNext = Math.min(discountableNext, coupon.discount.amount[this.items.currency]);
// Falls back to zero if the coupon does not support the checkout currency
const discountAmount = coupon.discount.amount[this.items.currency] || 0;
discountNow = Math.min(discountableNow, discountAmount);
discountNext = Math.min(discountableNext, discountAmount);
}
}
return { discountNow, discountNext };
Expand Down
46 changes: 24 additions & 22 deletions lib/recurly/pricing/checkout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,27 +259,29 @@ export default class CheckoutPricing extends Pricing {
this.emit('unset.coupon');
};

return new PricingPromise((resolve, reject) => {
if (this.items.coupon) this.remove({ coupon: this.items.coupon.code });

// A blank coupon is handled as ok
if (!couponCode) {
unset();
return resolve();
}

this.recurly.coupon({ plans: this.subscriptionPlanCodes, coupon: couponCode }, (err, coupon) => {
if (err && err.code === 'not-found') unset();
if (err) {
return this.error(err, reject, 'coupon');
} else {
debug('set.coupon');
this.items.coupon = coupon;
this.emit('set.coupon', coupon);
resolve(coupon);
return new PricingPromise(resolve => resolve(), this)
.then(() => {
if (this.items.coupon) return this.remove({ coupon: this.items.coupon.code });
})
.then(() => new PricingPromise((resolve, reject) => {
// A blank coupon is handled as ok
if (!couponCode) {
unset();
return resolve();
}
});
}, this);

this.recurly.coupon({ plans: this.subscriptionPlanCodes, coupon: couponCode }, (err, coupon) => {
if (err && err.code === 'not-found') unset();
if (err) {
return this.error(err, reject, 'coupon');
} else {
debug('set.coupon');
this.items.coupon = coupon;
this.emit('set.coupon', coupon);
resolve(coupon);
}
});
}));
}

/**
Expand Down Expand Up @@ -377,9 +379,9 @@ export default class CheckoutPricing extends Pricing {
if (options.coupon) {
// Remove the coupon from embedded subscriptions
Promise.all(this.validSubscriptions.map(sub => sub.coupon().reprice({ internal: true })))
.then(() => resolve(super.remove(options)));
.then(() => super.remove(options).then(resolve, reject));
} else {
resolve(super.remove(options));
super.remove(options).then(resolve, reject);
}
}
});
Expand Down
1 change: 1 addition & 0 deletions lib/recurly/pricing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class Pricing extends Emitter {
reason: 'does not exist on this pricing instance.'
}), reject);
}
resolve();
}, this).nodeify(done);
}

Expand Down
29 changes: 26 additions & 3 deletions test/pricing/checkout/checkout.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,24 @@ describe('CheckoutPricing', function () {
assert.equal(coupon.code, valid);
done();
});
this.pricing.coupon('coop').done();
this.pricing.coupon(valid).done();
});
});

describe('when given a valid coupon code that does not support the checkout currency', () => {
const incompatible = 'coop-fixed-all-500-eur';
it('does not discount the checkout', function (done) {
this.pricing
.coupon(incompatible)
.done(price => {
assert.equal(this.pricing.items.coupon.code, incompatible);
assert.equal(this.pricing.items.currency, 'USD');
assert.equal('USD' in this.pricing.items.coupon.discount.amount, false);
assert.equal('EUR' in this.pricing.items.coupon.discount.amount, true);
assert.equal(price.now.discount, 0);
assert.equal(price.next.discount, 0);
done();
});
});
});

Expand All @@ -527,12 +544,18 @@ describe('CheckoutPricing', function () {

it(`accepts a blank coupon code and unsets the existing coupon,
firing the unset.coupon event`, function (done) {
const part = after(2, done);
const errorSpy = sinon.spy();
assert.equal(this.pricing.items.coupon.code, 'coop');
this.pricing.on('unset.coupon', () => {
assert.equal(this.pricing.items.coupon, undefined);
done();
part();
});
this.pricing.on('error', errorSpy);
this.pricing.coupon().done(price => {
assert.equal(errorSpy.callCount, 0);
part();
});
this.pricing.coupon().done();
});
});

Expand Down
16 changes: 16 additions & 0 deletions test/server/fixtures/coupons/coop-fixed-all-500-eur.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"code": "coop-fixed-all-500-eur",
"name": "Test coupon: EUR 500 off all charges",
"discount": {
"type": "dollars",
"amount": {
"EUR": 500
}
},
"plans": [],
"single_use": false,
"applies_to_non_plan_charges": true,
"applies_to_plans": true,
"applies_to_all_plans": true,
"redemption_resource": "account"
}

0 comments on commit 3fb70b8

Please sign in to comment.