mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-24 01:29:35 +02: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,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -65,7 +65,7 @@ function PreviewSubscriptionChange() {
|
||||
<Col xs={3} className="text-right">
|
||||
<strong>
|
||||
{formatCurrencyLocalized(
|
||||
preview.immediateCharge,
|
||||
preview.immediateCharge.total,
|
||||
preview.currency
|
||||
)}
|
||||
</strong>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user