mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 14:43:46 +01:00
Merge pull request #21957 from overleaf/ls-compute-immediate-charge-for-subscription-update
compute immediate charge for subscription update GitOrigin-RevId: 4e5162660b26e6e9db69827a59aa8e0048fa7d5d
This commit is contained in:
@@ -14,6 +14,7 @@ const {
|
|||||||
CreditCardPaymentMethod,
|
CreditCardPaymentMethod,
|
||||||
RecurlyAddOn,
|
RecurlyAddOn,
|
||||||
RecurlyPlan,
|
RecurlyPlan,
|
||||||
|
RecurlyImmediateCharge,
|
||||||
} = require('./RecurlyEntities')
|
} = require('./RecurlyEntities')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -315,21 +316,42 @@ function subscriptionChangeFromApi(subscription, subscriptionChange) {
|
|||||||
subscriptionAddOnFromApi
|
subscriptionAddOnFromApi
|
||||||
)
|
)
|
||||||
|
|
||||||
let immediateCharge =
|
|
||||||
subscriptionChange.invoiceCollection?.chargeInvoice?.total ?? 0
|
|
||||||
for (const creditInvoice of subscriptionChange.invoiceCollection
|
|
||||||
?.creditInvoices ?? []) {
|
|
||||||
// The credit invoice totals are already negative
|
|
||||||
immediateCharge += creditInvoice.total ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RecurlySubscriptionChange({
|
return new RecurlySubscriptionChange({
|
||||||
subscription,
|
subscription,
|
||||||
nextPlanCode: subscriptionChange.plan.code,
|
nextPlanCode: subscriptionChange.plan.code,
|
||||||
nextPlanName: subscriptionChange.plan.name,
|
nextPlanName: subscriptionChange.plan.name,
|
||||||
nextPlanPrice: subscriptionChange.unitAmount,
|
nextPlanPrice: subscriptionChange.unitAmount,
|
||||||
nextAddOns,
|
nextAddOns,
|
||||||
immediateCharge,
|
immediateCharge: computeImmediateCharge(subscriptionChange),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute immediate charge based on invoice collection
|
||||||
|
*
|
||||||
|
* @param {recurly.SubscriptionChange} subscriptionChange - the subscription change returned from the API
|
||||||
|
* @return {RecurlyImmediateCharge}
|
||||||
|
*/
|
||||||
|
function computeImmediateCharge(subscriptionChange) {
|
||||||
|
const roundToTwoDecimal = (/** @type {number} */ num) =>
|
||||||
|
Math.round(num * 100) / 100
|
||||||
|
let subtotal =
|
||||||
|
subscriptionChange.invoiceCollection?.chargeInvoice?.subtotal ?? 0
|
||||||
|
let tax = subscriptionChange.invoiceCollection?.chargeInvoice?.tax ?? 0
|
||||||
|
let total = subscriptionChange.invoiceCollection?.chargeInvoice?.total ?? 0
|
||||||
|
for (const creditInvoice of subscriptionChange.invoiceCollection
|
||||||
|
?.creditInvoices ?? []) {
|
||||||
|
// The credit invoice numbers are already negative
|
||||||
|
subtotal = roundToTwoDecimal(subtotal + (creditInvoice.subtotal ?? 0))
|
||||||
|
total = roundToTwoDecimal(total + (creditInvoice.total ?? 0))
|
||||||
|
// Tax rate can be different in credit invoice if a user relocates
|
||||||
|
tax = roundToTwoDecimal(tax + (creditInvoice.tax ?? 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RecurlyImmediateCharge({
|
||||||
|
subtotal,
|
||||||
|
total,
|
||||||
|
tax,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class RecurlySubscriptionChange {
|
|||||||
* @param {string} props.nextPlanName
|
* @param {string} props.nextPlanName
|
||||||
* @param {number} props.nextPlanPrice
|
* @param {number} props.nextPlanPrice
|
||||||
* @param {RecurlySubscriptionAddOn[]} props.nextAddOns
|
* @param {RecurlySubscriptionAddOn[]} props.nextAddOns
|
||||||
* @param {number} [props.immediateCharge]
|
* @param {RecurlyImmediateCharge} [props.immediateCharge]
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
this.subscription = props.subscription
|
this.subscription = props.subscription
|
||||||
@@ -260,7 +260,9 @@ class RecurlySubscriptionChange {
|
|||||||
this.nextPlanName = props.nextPlanName
|
this.nextPlanName = props.nextPlanName
|
||||||
this.nextPlanPrice = props.nextPlanPrice
|
this.nextPlanPrice = props.nextPlanPrice
|
||||||
this.nextAddOns = props.nextAddOns
|
this.nextAddOns = props.nextAddOns
|
||||||
this.immediateCharge = props.immediateCharge ?? 0
|
this.immediateCharge =
|
||||||
|
props.immediateCharge ??
|
||||||
|
new RecurlyImmediateCharge({ subtotal: 0, tax: 0, total: 0 })
|
||||||
|
|
||||||
this.subtotal = this.nextPlanPrice
|
this.subtotal = this.nextPlanPrice
|
||||||
for (const addOn of this.nextAddOns) {
|
for (const addOn of this.nextAddOns) {
|
||||||
@@ -299,6 +301,20 @@ class CreditCardPaymentMethod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RecurlyImmediateCharge {
|
||||||
|
/**
|
||||||
|
* @param {object} props
|
||||||
|
* @param {number} props.subtotal
|
||||||
|
* @param {number} props.tax
|
||||||
|
* @param {number} props.total
|
||||||
|
*/
|
||||||
|
constructor(props) {
|
||||||
|
this.subtotal = props.subtotal
|
||||||
|
this.tax = props.tax
|
||||||
|
this.total = props.total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An add-on configuration, independent of any subscription
|
* An add-on configuration, independent of any subscription
|
||||||
*/
|
*/
|
||||||
@@ -350,4 +366,5 @@ module.exports = {
|
|||||||
RecurlyAddOn,
|
RecurlyAddOn,
|
||||||
RecurlyPlan,
|
RecurlyPlan,
|
||||||
isStandaloneAiAddOnPlanCode,
|
isStandaloneAiAddOnPlanCode,
|
||||||
|
RecurlyImmediateCharge,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -915,7 +915,7 @@ function makeChangePreview(
|
|||||||
return {
|
return {
|
||||||
change: subscriptionChangeDescription,
|
change: subscriptionChangeDescription,
|
||||||
currency: subscription.currency,
|
currency: subscription.currency,
|
||||||
immediateCharge: subscriptionChange.immediateCharge,
|
immediateCharge: { ...subscriptionChange.immediateCharge },
|
||||||
paymentMethod: paymentMethod.toString(),
|
paymentMethod: paymentMethod.toString(),
|
||||||
nextPlan: {
|
nextPlan: {
|
||||||
annual: nextPlan.annual ?? false,
|
annual: nextPlan.annual ?? false,
|
||||||
@@ -966,6 +966,7 @@ module.exports = {
|
|||||||
previewAddonPurchase: expressify(previewAddonPurchase),
|
previewAddonPurchase: expressify(previewAddonPurchase),
|
||||||
purchaseAddon,
|
purchaseAddon,
|
||||||
removeAddon,
|
removeAddon,
|
||||||
|
makeChangePreview,
|
||||||
promises: {
|
promises: {
|
||||||
getRecommendedCurrency: _getRecommendedCurrency,
|
getRecommendedCurrency: _getRecommendedCurrency,
|
||||||
getLatamCountryBannerDetails,
|
getLatamCountryBannerDetails,
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ function PreviewSubscriptionChange() {
|
|||||||
<Col xs={3} className="text-right">
|
<Col xs={3} className="text-right">
|
||||||
<strong>
|
<strong>
|
||||||
{formatCurrencyLocalized(
|
{formatCurrencyLocalized(
|
||||||
preview.immediateCharge,
|
preview.immediateCharge.total,
|
||||||
preview.currency
|
preview.currency
|
||||||
)}
|
)}
|
||||||
</strong>
|
</strong>
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ describe('RecurlyClient', function () {
|
|||||||
this.client = client = {
|
this.client = client = {
|
||||||
getAccount: sinon.stub(),
|
getAccount: sinon.stub(),
|
||||||
listAccountSubscriptions: sinon.stub(),
|
listAccountSubscriptions: sinon.stub(),
|
||||||
|
previewSubscriptionChange: sinon.stub(),
|
||||||
}
|
}
|
||||||
this.recurly = {
|
this.recurly = {
|
||||||
errors: recurly.errors,
|
errors: recurly.errors,
|
||||||
@@ -381,4 +382,65 @@ describe('RecurlyClient', function () {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('previewSubscriptionChange', function () {
|
||||||
|
describe('compute immediate charge', function () {
|
||||||
|
it('only has charge invoice', async function () {
|
||||||
|
this.client.previewSubscriptionChange.resolves({
|
||||||
|
plan: { code: 'test_code', name: 'test name' },
|
||||||
|
unitAmount: 0,
|
||||||
|
invoiceCollection: {
|
||||||
|
chargeInvoice: {
|
||||||
|
subtotal: 100,
|
||||||
|
tax: 20,
|
||||||
|
total: 120,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { immediateCharge } =
|
||||||
|
await this.RecurlyClient.promises.previewSubscriptionChange(
|
||||||
|
new RecurlySubscriptionChangeRequest({
|
||||||
|
subscription: this.subscription,
|
||||||
|
timeframe: 'now',
|
||||||
|
planCode: 'new-plan',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(immediateCharge.subtotal).to.be.equal(100)
|
||||||
|
expect(immediateCharge.tax).to.be.equal(20)
|
||||||
|
expect(immediateCharge.total).to.be.equal(120)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('credit invoice with imprecise float number', async function () {
|
||||||
|
this.client.previewSubscriptionChange.resolves({
|
||||||
|
plan: { code: 'test_code', name: 'test name' },
|
||||||
|
unitAmount: 0,
|
||||||
|
invoiceCollection: {
|
||||||
|
chargeInvoice: {
|
||||||
|
subtotal: 100.3,
|
||||||
|
tax: 20.3,
|
||||||
|
total: 120.3,
|
||||||
|
},
|
||||||
|
creditInvoices: [
|
||||||
|
{
|
||||||
|
subtotal: -20.1,
|
||||||
|
tax: -4.1,
|
||||||
|
total: -24.1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { immediateCharge } =
|
||||||
|
await this.RecurlyClient.promises.previewSubscriptionChange(
|
||||||
|
new RecurlySubscriptionChangeRequest({
|
||||||
|
subscription: this.subscription,
|
||||||
|
timeframe: 'now',
|
||||||
|
planCode: 'new-plan',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(immediateCharge.subtotal).to.be.equal(80.2)
|
||||||
|
expect(immediateCharge.tax).to.be.equal(16.2)
|
||||||
|
expect(immediateCharge.total).to.be.equal(96.2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ export type SubscriptionChangePreview = {
|
|||||||
change: SubscriptionChangeDescription
|
change: SubscriptionChangeDescription
|
||||||
currency: string
|
currency: string
|
||||||
paymentMethod: string
|
paymentMethod: string
|
||||||
immediateCharge: number
|
|
||||||
nextPlan: {
|
nextPlan: {
|
||||||
annual: boolean
|
annual: boolean
|
||||||
}
|
}
|
||||||
|
immediateCharge: {
|
||||||
|
subtotal: number
|
||||||
|
tax: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
nextInvoice: {
|
nextInvoice: {
|
||||||
date: string
|
date: string
|
||||||
plan: {
|
plan: {
|
||||||
|
|||||||
Reference in New Issue
Block a user