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:
Liangjun Song
2024-11-21 11:20:01 +00:00
committed by Copybot
parent 583923a4d0
commit ac8d8d6edc
6 changed files with 120 additions and 14 deletions

View File

@@ -14,6 +14,7 @@ const {
CreditCardPaymentMethod,
RecurlyAddOn,
RecurlyPlan,
RecurlyImmediateCharge,
} = require('./RecurlyEntities')
/**
@@ -315,21 +316,42 @@ function subscriptionChangeFromApi(subscription, subscriptionChange) {
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({
subscription,
nextPlanCode: subscriptionChange.plan.code,
nextPlanName: subscriptionChange.plan.name,
nextPlanPrice: subscriptionChange.unitAmount,
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,
})
}

View File

@@ -252,7 +252,7 @@ class RecurlySubscriptionChange {
* @param {string} props.nextPlanName
* @param {number} props.nextPlanPrice
* @param {RecurlySubscriptionAddOn[]} props.nextAddOns
* @param {number} [props.immediateCharge]
* @param {RecurlyImmediateCharge} [props.immediateCharge]
*/
constructor(props) {
this.subscription = props.subscription
@@ -260,7 +260,9 @@ class RecurlySubscriptionChange {
this.nextPlanName = props.nextPlanName
this.nextPlanPrice = props.nextPlanPrice
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
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
*/
@@ -350,4 +366,5 @@ module.exports = {
RecurlyAddOn,
RecurlyPlan,
isStandaloneAiAddOnPlanCode,
RecurlyImmediateCharge,
}

View File

@@ -915,7 +915,7 @@ function makeChangePreview(
return {
change: subscriptionChangeDescription,
currency: subscription.currency,
immediateCharge: subscriptionChange.immediateCharge,
immediateCharge: { ...subscriptionChange.immediateCharge },
paymentMethod: paymentMethod.toString(),
nextPlan: {
annual: nextPlan.annual ?? false,
@@ -966,6 +966,7 @@ module.exports = {
previewAddonPurchase: expressify(previewAddonPurchase),
purchaseAddon,
removeAddon,
makeChangePreview,
promises: {
getRecommendedCurrency: _getRecommendedCurrency,
getLatamCountryBannerDetails,

View File

@@ -65,7 +65,7 @@ function PreviewSubscriptionChange() {
<Col xs={3} className="text-right">
<strong>
{formatCurrencyLocalized(
preview.immediateCharge,
preview.immediateCharge.total,
preview.currency
)}
</strong>

View File

@@ -98,6 +98,7 @@ describe('RecurlyClient', function () {
this.client = client = {
getAccount: sinon.stub(),
listAccountSubscriptions: sinon.stub(),
previewSubscriptionChange: sinon.stub(),
}
this.recurly = {
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)
})
})
})
})

View File

@@ -2,10 +2,14 @@ export type SubscriptionChangePreview = {
change: SubscriptionChangeDescription
currency: string
paymentMethod: string
immediateCharge: number
nextPlan: {
annual: boolean
}
immediateCharge: {
subtotal: number
tax: number
total: number
}
nextInvoice: {
date: string
plan: {