Merge pull request #36475 from overleaf/revert-36409-ac-remove-subscription-formatters

Revert "[web] Remove SubscriptionFormatters, format subscription dates in the frontend"

GitOrigin-RevId: d4c6bb600f7315c27016a45a8250a61b46de0119
This commit is contained in:
Antoine Clausse
2026-09-17 11:52:51 +00:00
committed by Copybot
parent c461297b13
commit 07835f92b3
26 changed files with 203 additions and 149 deletions
@@ -0,0 +1,20 @@
import dateformat from 'dateformat'
function formatDateTime(date) {
if (!date) {
return null
}
return dateformat(date, 'mmmm dS, yyyy h:MM TT Z', true)
}
function formatDate(date) {
if (!date) {
return null
}
return dateformat(date, 'mmmm dS, yyyy', true)
}
export default {
formatDateTime,
formatDate,
}
@@ -5,6 +5,7 @@ import PlansLocator from './PlansLocator.mjs'
import { getLocalizedPlanPricing } from './PriceVersions.mjs'
import { isStandaloneAiAddOnPlanCode } from './AiHelper.mjs'
import PaymentProviderEntities from './PaymentProviderEntities.mjs'
import SubscriptionFormatters from './SubscriptionFormatters.mjs'
import SubscriptionLocator from './SubscriptionLocator.mjs'
import InstitutionsGetter from '../Institutions/InstitutionsGetter.mjs'
import InstitutionsManager from '../Institutions/InstitutionsManager.mjs'
@@ -256,9 +257,9 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
}
})
const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
const isInTrial = SubscriptionHelper.isInTrial(
paymentRecord.subscription.trialPeriodEnd
)
const isInTrial =
paymentRecord.subscription.trialPeriodEnd &&
paymentRecord.subscription.trialPeriodEnd.getTime() > Date.now()
let isEligibleForPause = false
const commonPauseConditions =
@@ -301,10 +302,18 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
additionalLicenses,
addOns,
totalLicenses,
periodEnd: paymentRecord.subscription.periodEnd,
nextPaymentDueAt: SubscriptionFormatters.formatDateTime(
paymentRecord.subscription.periodEnd
),
nextPaymentDueDate: SubscriptionFormatters.formatDate(
paymentRecord.subscription.periodEnd
),
currency: paymentRecord.subscription.currency,
planPrice: paymentRecord.subscription.planPrice,
state: paymentRecord.subscription.state,
trialEndsAtFormatted: SubscriptionFormatters.formatDateTime(
paymentRecord.subscription.trialPeriodEnd
),
trialEndsAt: paymentRecord.subscription.trialPeriodEnd,
activeCoupons,
accountEmail: paymentRecord.account.email,
@@ -17,7 +17,6 @@ import * as eventTracking from '../../../../infrastructure/event-tracking'
import PauseDuck from '../../images/pause-duck.svg'
import GenericErrorAlert from './generic-error-alert'
import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
import isInFreeTrial from '../../util/is-in-free-trial'
const pauseMonthDurationOptions = [1, 2, 3]
@@ -40,7 +39,10 @@ export default function PauseSubscriptionModal() {
const subscription = personalSubscription as PaidSubscription
eventTracking.sendMB('subscription-page-cancel-button-click', {
plan_code: subscription?.planCode,
is_trial: isInFreeTrial(subscription?.payment.trialEndsAt),
is_trial:
subscription?.payment.trialEndsAtFormatted &&
subscription?.payment.trialEndsAt &&
new Date(subscription.payment.trialEndsAt).getTime() > Date.now(),
})
setShowCancellation(true)
}
@@ -30,10 +30,6 @@ import { useLocation } from '@/shared/hooks/use-location'
import { FlashMessage } from '@/features/subscription/components/dashboard/states/active/flash-message'
import Notification from '@/shared/components/notification'
import { PendingPlanChange } from './pending-plan-change'
import {
formatPaymentDate,
formatPaymentDateTime,
} from '../../../../util/payment-dates'
export function ActiveSubscription({
subscription,
@@ -142,7 +138,7 @@ export function ActiveSubscription({
<p className="mb-1" data-testid="renews-on">
<Trans
i18nKey="renews_on"
values={{ date: formatPaymentDate(subscription.payment.periodEnd) }}
values={{ date: subscription.payment.nextPaymentDueDate }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[<strong />]} // eslint-disable-line react/jsx-key
@@ -202,12 +198,13 @@ export function ActiveSubscription({
<hr />
<h2 className="h3 fw-bold">{t('plan')}</h2>
<h3 className="h5 mt-0 mb-1 fw-bold">{planName}</h3>
{isInFreeTrial(subscription.payment.trialEndsAt) && (
<TrialEnding
trialEndsAt={subscription.payment.trialEndsAt}
className="mb-1"
/>
)}
{isInFreeTrial(subscription.payment.trialEndsAt) &&
subscription.payment.trialEndsAtFormatted && (
<TrialEnding
trialEndsAtFormatted={subscription.payment.trialEndsAtFormatted}
className="mb-1"
/>
)}
{subscription.payment.totalLicenses > 0 && (
<p className="mb-1" data-testid="plan-licenses">
{isLegacyPlan &&
@@ -248,9 +245,7 @@ export function ActiveSubscription({
i18nKey="your_subscription_will_pause_on"
values={{
planName: subscription.plan.name,
pauseDate: formatPaymentDateTime(
subscription.payment.periodEnd
),
pauseDate: subscription.payment.nextPaymentDueAt,
reactivationDate: getFormattedRenewalDate(),
}}
shouldUnescape
@@ -22,7 +22,6 @@ import sparkle from '@/shared/svgs/sparkle.svg'
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import { LICENSE_ADD_ON } from '@/features/group-management/components/upgrade-subscription/upgrade-subscription-plan-details'
import WritefullManagedBundleAddOn from './change-plan/modals/writefull-bundle-management-modal'
import { formatPaymentDate } from '../../../../util/payment-dates'
type AddOnsProps = {
subscription: PaidSubscription
@@ -190,7 +189,7 @@ function AddOns({
)
}
displayPrice={addOnsDisplayPrices[addOn.addOnCode]}
nextBillingDate={formatPaymentDate(subscription.payment.periodEnd)!}
nextBillingDate={subscription.payment.nextPaymentDueDate}
/>
))}
{hasAiAssistViaWritefull && <WritefullManagedBundleAddOn />}
@@ -6,7 +6,6 @@ import { PaidSubscription } from '../../../../../../../../types/subscription/das
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useLocation } from '@/shared/hooks/use-location'
import { stripHasSubscription } from '../../../../data/subscription-url'
import isInFreeTrial from '../../../../util/is-in-free-trial'
export function CancelSubscriptionButton() {
const { t } = useTranslation()
@@ -19,7 +18,10 @@ export function CancelSubscriptionButton() {
} = useSubscriptionDashboardContext()
const subscription = personalSubscription as PaidSubscription
const isInTrial = isInFreeTrial(subscription?.payment.trialEndsAt)
const isInTrial =
subscription?.payment.trialEndsAtFormatted &&
subscription?.payment.trialEndsAt &&
new Date(subscription.payment.trialEndsAt).getTime() > Date.now()
const hasPendingOrActivePause =
subscription.payment.state === 'paused' ||
(subscription.payment.state === 'active' &&
@@ -4,7 +4,6 @@ import { Trans, useTranslation } from 'react-i18next'
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import { useEffect, useState } from 'react'
import { useLocation } from '@/shared/hooks/use-location'
import { formatPaymentDateTime } from '../../../../util/payment-dates'
export type FlashMessageName = 'paused' | 'unpaused' | 'error'
@@ -37,9 +36,7 @@ export function FlashMessage() {
<Trans
i18nKey="your_subscription_will_pause_on_short"
values={{
pauseDate: formatPaymentDateTime(
subscription.payment.periodEnd
),
pauseDate: subscription.payment.nextPaymentDueAt,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
@@ -1,10 +1,5 @@
import { Trans } from 'react-i18next'
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import isInFreeTrial from '../../../../util/is-in-free-trial'
import {
formatPaymentDate,
formatPaymentDateTime,
} from '../../../../util/payment-dates'
type SubscriptionRemainderProps = {
subscription: PaidSubscription
@@ -15,11 +10,14 @@ function SubscriptionRemainder({
subscription,
hideTime,
}: SubscriptionRemainderProps) {
const stillInATrial = isInFreeTrial(subscription.payment.trialEndsAt)
const stillInATrial =
subscription.payment.trialEndsAtFormatted &&
subscription.payment.trialEndsAt &&
new Date(subscription.payment.trialEndsAt).getTime() > Date.now()
const terminationDate = hideTime
? formatPaymentDate(subscription.payment.periodEnd)
: formatPaymentDateTime(subscription.payment.periodEnd)
? subscription.payment.nextPaymentDueDate
: subscription.payment.nextPaymentDueAt
return stillInATrial ? (
<Trans
i18nKey="subscription_will_remain_active_until_end_of_trial_period_x"
@@ -1,17 +1,19 @@
import { Trans } from 'react-i18next'
import { formatPaymentDateTime } from '../../../../util/payment-dates'
type TrialEndingProps = {
trialEndsAt: string
trialEndsAtFormatted: string
className?: string
}
export function TrialEnding({ trialEndsAt, className }: TrialEndingProps) {
export function TrialEnding({
trialEndsAtFormatted,
className,
}: TrialEndingProps) {
return (
<p className={className} data-testid="trial-ending">
<Trans
i18nKey="youre_on_free_trial_which_ends_on"
values={{ date: formatPaymentDateTime(trialEndsAt) }}
values={{ date: trialEndsAtFormatted }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
@@ -6,7 +6,6 @@ import {
} from '../../../data/add-on-codes'
import ReactivateSubscription from '../reactivate-subscription'
import OLButton from '@/shared/components/ol/ol-button'
import { formatPaymentDateTime } from '../../../util/payment-dates'
export function CanceledSubscription({
subscription,
@@ -49,9 +48,7 @@ export function CanceledSubscription({
<Trans
i18nKey="subscription_canceled_and_terminate_on_x"
values={{
terminateDate: formatPaymentDateTime(
subscription.payment.periodEnd
),
terminateDate: subscription.payment.nextPaymentDueAt,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
@@ -17,7 +17,6 @@ import Notification from '@/shared/components/notification'
import WritefullManagedBundleAddOn from './states/active/change-plan/modals/writefull-bundle-management-modal'
import RedirectAlerts from './redirect-alerts'
import { PaidSubscription } from '@ol-types/subscription/dashboard/subscription'
import { formatPaymentDate } from '../../util/payment-dates'
function SubscriptionDashboard() {
const { t } = useTranslation()
@@ -43,7 +42,7 @@ function SubscriptionDashboard() {
subscription &&
subscription.pendingPlan &&
subscription.pendingPlan.name !== subscription.plan.name
const nextPaymentDueDate = formatPaymentDate(subscription?.payment?.periodEnd)
const nextPaymentDueDate = subscription?.payment?.nextPaymentDueDate
return (
<div className="container">
@@ -15,7 +15,6 @@ import {
import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user'
import { getUpgradePlanDisplayName } from '../../util/plan-display-names'
import { formatPaymentDateTime } from '../../util/payment-dates'
function SuccessfulSubscription() {
const { t } = useTranslation()
@@ -57,9 +56,8 @@ function SuccessfulSubscription() {
i18nKey="next_payment_of_x_collectected_on_y"
values={{
paymentAmmount: subscription.payment.displayPrice,
collectionDate: formatPaymentDateTime(
subscription.payment.periodEnd
),
collectionDate:
subscription.payment.nextPaymentDueAt,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
@@ -177,9 +175,7 @@ function UpgradeSuccess({
i18nKey="next_payment_of_x_collectected_on_y"
values={{
paymentAmmount: subscription.payment.displayPrice,
collectionDate: formatPaymentDateTime(
subscription.payment.periodEnd
),
collectionDate: subscription.payment.nextPaymentDueAt,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
@@ -33,7 +33,6 @@ import { formatCurrency } from '@/shared/utils/currency'
import { ManagedInstitution } from '../../../../../types/subscription/dashboard/managed-institution'
import { Publisher } from '../../../../../types/subscription/dashboard/publisher'
import { formatTime } from '@/features/utils/format-date'
import { formatPaymentDateTime } from '../util/payment-dates'
type SubscriptionDashboardContextValue = {
groupPlanToChangeToCode: string
@@ -151,7 +150,7 @@ export function SubscriptionDashboardProvider({
!personalSubscription.payment.pausedAt ||
!personalSubscription.payment.remainingPauseCycles
) {
return formatPaymentDateTime(personalSubscription.payment.periodEnd)!
return personalSubscription.payment.nextPaymentDueAt
}
const pausedDate = new Date(personalSubscription.payment.pausedAt)
pausedDate.setMonth(
@@ -1,6 +1,4 @@
export default function isInFreeTrial(
trialEndsAt?: string | null
): trialEndsAt is string {
export default function isInFreeTrial(trialEndsAt?: string | null) {
if (!trialEndsAt) return false
const endDate = new Date(trialEndsAt)
@@ -1,15 +0,0 @@
import { formatTime } from '@/features/utils/format-date'
export function formatPaymentDate(date: string | null | undefined) {
if (!date) {
return null
}
return formatTime(date, 'MMMM Do, YYYY', true)
}
export function formatPaymentDateTime(date: string | null | undefined) {
if (!date) {
return null
}
return formatTime(date, 'MMMM Do, YYYY h:mm A [UTC]', true)
}
+2
View File
@@ -130,6 +130,7 @@
"crc-32": "^1.2.2",
"csurf": "patch:csurf@npm%3A1.11.0#~/.yarn/patches/csurf-npm-1.11.0-c1b9cbb35b.patch",
"csv": "^6.2.5",
"dateformat": "1.0.4-1.2.3",
"ejs": "^3.1.10",
"email-addresses": "^5.0.0",
"eventsource-parser": "^1.1.2",
@@ -263,6 +264,7 @@
"@types/bootstrap": "^5.2.10",
"@types/chai": "^4.3.0",
"@types/chai-as-promised": "^8.0.2",
"@types/dateformat": "^5.0.3",
"@types/diff": "^5.2.3",
"@types/dom-speech-recognition": "^0.0.7",
"@types/events": "^3.0.3",
@@ -22,7 +22,6 @@ import { reactivateSubscriptionUrl } from '../../../../../../frontend/js/feature
import fetchMock from 'fetch-mock'
import sinon from 'sinon'
import { location } from '@/shared/components/location'
import { formatPaymentDateTime } from '@/features/subscription/util/payment-dates'
describe('<PersonalSubscription />', function () {
afterEach(function () {
@@ -78,12 +77,9 @@ describe('<PersonalSubscription />', function () {
'Your subscription has been canceled and will terminate on',
{ exact: false }
)
screen.getByText(
formatPaymentDateTime(canceledSubscription.payment!.periodEnd)!,
{
exact: false,
}
)
screen.getByText(canceledSubscription.payment!.nextPaymentDueAt, {
exact: false,
})
screen.getByText('No further payments will be taken.', { exact: false })
@@ -27,10 +27,6 @@ import {
} from '@/features/subscription/data/subscription-url'
import { location } from '@/shared/components/location'
import { MetaTag } from '@/utils/meta'
import {
formatPaymentDate,
formatPaymentDateTime,
} from '@/features/subscription/util/payment-dates'
describe('<ActiveSubscription />', function () {
let sendMBSpy: sinon.SinonSpy
@@ -79,7 +75,7 @@ describe('<ActiveSubscription />', function () {
within(screen.getByTestId('renews-on')).getByText((_, el) =>
Boolean(
el?.textContent?.includes(
`Renews on ${formatPaymentDate(subscription.payment.periodEnd)}`
`Renews on ${subscription.payment.nextPaymentDueDate}`
)
)
)
@@ -214,7 +210,7 @@ describe('<ActiveSubscription />', function () {
within(screen.getByTestId('trial-ending')).getByText((_, el) =>
Boolean(
el?.textContent?.includes(
`You’re on a free trial which ends on ${formatPaymentDateTime(trialSubscription.payment.trialEndsAt)}`
`You’re on a free trial which ends on ${trialSubscription.payment.trialEndsAtFormatted}`
)
)
)
@@ -292,7 +288,7 @@ describe('<ActiveSubscription />', function () {
{ exact: false }
)
const dates = screen.getAllByText(
formatPaymentDate(annualActiveSubscription.payment.periodEnd)!,
annualActiveSubscription.payment.nextPaymentDueAt,
{
exact: false,
}
@@ -311,10 +307,7 @@ describe('<ActiveSubscription />', function () {
{ exact: false }
)
const dates = screen.getAllByText(
formatPaymentDate(trialSubscription.payment.trialEndsAt)!,
{
exact: false,
}
trialSubscription.payment.trialEndsAtFormatted!
)
expect(dates.length).to.equal(3)
const button = screen.getByRole('button', {
@@ -9,7 +9,6 @@ import {
} from '../../fixtures/subscriptions'
import { ExposedSettings } from '../../../../../../types/exposed-settings'
import { UserProvider } from '@/shared/context/user-context'
import { formatPaymentDateTime } from '@/features/subscription/util/payment-dates'
describe('successful subscription page', function () {
it('renders the invoices link', function () {
@@ -123,7 +122,7 @@ describe('successful subscription page', function () {
.getByText(/the next payment of/i)
.textContent?.replace(/\xA0/g, ' ')
).to.equal(
`The next payment of ${annualActiveSubscriptionPro.payment.displayPrice} will be collected on ${formatPaymentDateTime(annualActiveSubscriptionPro.payment.periodEnd)}.`
`The next payment of ${annualActiveSubscriptionPro.payment.displayPrice} will be collected on ${annualActiveSubscriptionPro.payment.nextPaymentDueAt}.`
)
screen.getByText(/taxes may be added, depending on your billing address/i)
@@ -3,11 +3,17 @@ import {
GroupSubscription,
PaidSubscription,
} from '../../../../../types/subscription/dashboard/subscription'
import dateformat from 'dateformat'
const today = new Date()
const oneYearFromToday = new Date().setFullYear(today.getFullYear() + 1)
const periodEnd = new Date(oneYearFromToday).toISOString()
const nextPaymentDueAt = dateformat(oneYearFromToday, 'dS mmmm yyyy')
const nextPaymentDueDate = dateformat(oneYearFromToday, 'dS mmmm yyyy')
const sevenDaysFromToday = new Date().setDate(today.getDate() + 7)
const sevenDaysFromTodayISO = new Date(sevenDaysFromToday).toISOString()
const sevenDaysFromTodayFormatted = dateformat(
sevenDaysFromToday,
'dS mmmm yyyy'
)
export const annualActiveSubscription: PaidSubscription = {
manager_ids: ['abc123'],
@@ -33,9 +39,11 @@ export const annualActiveSubscription: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -76,9 +84,11 @@ export const pastDueActiveSubscription: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'past_due',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -119,9 +129,11 @@ export const annualActiveSubscriptionWithCoupons: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [
{ name: 'Coupon1', code: 'c1', description: 'Coupon1 for 10% off' },
@@ -165,10 +177,12 @@ export const pendingPausedSubscription: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
remainingPauseCycles: 1,
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -209,10 +223,12 @@ export const pausedSubscription: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'paused',
remainingPauseCycles: 1,
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -253,9 +269,11 @@ export const annualActiveSubscriptionWithAddons: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -299,9 +317,11 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'EUR',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -341,9 +361,11 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -384,9 +406,11 @@ export const pastDueExpiredSubscription: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'expired',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -427,9 +451,11 @@ export const canceledSubscription: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'canceled',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -470,9 +496,11 @@ export const pendingAddOnChange: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -527,9 +555,11 @@ export const pendingSubscriptionChange: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -581,9 +611,11 @@ export const groupActiveSubscription: GroupSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 10,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -628,9 +660,11 @@ export const groupProfessionalActiveSubscription: GroupSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 10,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -676,9 +710,11 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 11,
totalLicenses: 21,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'fake@example.com',
@@ -730,10 +766,12 @@ export const trialSubscription: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd: sevenDaysFromTodayISO,
nextPaymentDueAt: sevenDaysFromTodayFormatted,
nextPaymentDueDate: sevenDaysFromTodayFormatted,
currency: 'USD',
state: 'active',
trialEndsAt: sevenDaysFromTodayISO,
trialEndsAtFormatted: sevenDaysFromTodayFormatted,
trialEndsAt: new Date(sevenDaysFromToday).toString(),
activeCoupons: [],
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
@@ -793,10 +831,12 @@ export const trialCollaboratorSubscription: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd: sevenDaysFromTodayISO,
nextPaymentDueAt: sevenDaysFromTodayFormatted,
nextPaymentDueDate: sevenDaysFromTodayFormatted,
currency: 'USD',
state: 'active',
trialEndsAt: sevenDaysFromTodayISO,
trialEndsAtFormatted: sevenDaysFromTodayFormatted,
trialEndsAt: new Date(sevenDaysFromToday).toString(),
activeCoupons: [],
accountEmail: 'foo@example.com',
hasPastDueInvoice: false,
@@ -835,9 +875,11 @@ export const monthlyActiveCollaborator: PaidSubscription = {
accountManagementLink: '/user/subscription/payment/account-management',
additionalLicenses: 0,
totalLicenses: 0,
periodEnd,
nextPaymentDueAt,
nextPaymentDueDate,
currency: 'USD',
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'foo@example.com',
@@ -1,5 +1,6 @@
import { expect } from 'chai'
import isInFreeTrial from '../../../../../frontend/js/features/subscription/util/is-in-free-trial'
import dateformat from 'dateformat'
describe('isInFreeTrial', function () {
it('returns false when no date sent', function () {
@@ -14,6 +15,10 @@ describe('isInFreeTrial', function () {
it('returns true when date is in the future', function () {
const today = new Date()
const sevenDaysFromToday = new Date().setDate(today.getDate() + 7)
expect(isInFreeTrial(new Date(sevenDaysFromToday).toISOString())).to.be.true
const sevenDaysFromTodayFormatted = dateformat(
sevenDaysFromToday,
'dS mmmm yyyy'
)
expect(isInFreeTrial(sevenDaysFromTodayFormatted)).to.be.true
})
})
@@ -1,31 +0,0 @@
import { expect } from 'chai'
import {
formatPaymentDate,
formatPaymentDateTime,
} from '../../../../../frontend/js/features/subscription/util/payment-dates'
describe('payment-dates', function () {
describe('formatPaymentDate', function () {
it('formats a date in UTC', function () {
expect(formatPaymentDate('2025-02-20T12:00:00.000Z')).to.equal(
'February 20th, 2025'
)
})
it('returns null for missing dates', function () {
expect(formatPaymentDate(null)).to.be.null
expect(formatPaymentDate(undefined)).to.be.null
})
})
describe('formatPaymentDateTime', function () {
it('formats a date and time in UTC', function () {
expect(formatPaymentDateTime('2025-02-20T12:00:00.000Z')).to.equal(
'February 20th, 2025 12:00 PM UTC'
)
})
it('returns null for missing dates', function () {
expect(formatPaymentDateTime(null)).to.be.null
expect(formatPaymentDateTime(undefined)).to.be.null
})
})
})
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest'
import SubscriptionFormatters from '../../../../app/src/Features/Subscription/SubscriptionFormatters.mjs'
describe('SubscriptionFormatters', function () {
describe('formatDateTime', function () {
it('should return null if no date', function () {
const result = SubscriptionFormatters.formatDateTime(null)
expect(result).to.equal(null)
})
it('should format date with time', function () {
const date = new Date(1639904485000)
const result = SubscriptionFormatters.formatDateTime(date)
expect(result).to.equal('December 19th, 2021 9:01 AM UTC')
})
})
describe('formatDate', function () {
it('should return null if no date', function () {
const result = SubscriptionFormatters.formatDate(null)
expect(result).to.equal(null)
})
it('should format date', function () {
const date = new Date(1639904485000)
const result = SubscriptionFormatters.formatDate(date)
expect(result).to.equal('December 19th, 2021')
})
})
})
@@ -745,10 +745,12 @@ describe('SubscriptionViewModelBuilder', function () {
},
],
totalLicenses: 0,
periodEnd: new Date('2025-02-20T12:00:00.000Z'),
nextPaymentDueAt: 'February 20th, 2025 12:00 PM UTC',
nextPaymentDueDate: 'February 20th, 2025',
currency: 'EUR',
planPrice: 13,
state: 'active',
trialEndsAtFormatted: null,
trialEndsAt: null,
activeCoupons: [],
accountEmail: 'example@example.com',
@@ -37,10 +37,12 @@ type PaymentProviderRecord = {
additionalLicenses: number
addOns: PaymentProviderAddOn[]
totalLicenses: number
periodEnd: string
nextPaymentDueAt: string
nextPaymentDueDate: string
currency: CurrencyCode
planPrice: number
state?: SubscriptionState
trialEndsAtFormatted: Nullable<string>
trialEndsAt: Nullable<string>
activeCoupons: PaymentProviderCoupon[]
accountEmail: string
+16
View File
@@ -8006,6 +8006,7 @@ __metadata:
"@types/bootstrap": "npm:^5.2.10"
"@types/chai": "npm:^4.3.0"
"@types/chai-as-promised": "npm:^8.0.2"
"@types/dateformat": "npm:^5.0.3"
"@types/diff": "npm:^5.2.3"
"@types/dom-speech-recognition": "npm:^0.0.7"
"@types/events": "npm:^3.0.3"
@@ -8099,6 +8100,7 @@ __metadata:
cypress-multi-reporters: "npm:^2.0.5"
cypress-plugin-tab: "npm:^1.0.5"
d3: "npm:^3.5.16"
dateformat: "npm:1.0.4-1.2.3"
daterangepicker: "npm:2.1.27"
diff: "npm:^5.1.0"
dompurify: "npm:^3.3.2"
@@ -10793,6 +10795,13 @@ __metadata:
languageName: node
linkType: hard
"@types/dateformat@npm:^5.0.3":
version: 5.0.3
resolution: "@types/dateformat@npm:5.0.3"
checksum: 10c0/828452c096b0b6901a64ce1f3a47fb96755f2451a2605b59fb271407526c62020d171aab6946f9268ca5a4d63db7952c32595bf30b49ed54b218414ec6a02261
languageName: node
linkType: hard
"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.12, @types/debug@npm:^4.1.8":
version: 4.1.13
resolution: "@types/debug@npm:4.1.13"
@@ -16422,6 +16431,13 @@ __metadata:
languageName: node
linkType: hard
"dateformat@npm:1.0.4-1.2.3":
version: 1.0.4-1.2.3
resolution: "dateformat@npm:1.0.4-1.2.3"
checksum: 10c0/290dec607d80e0a7dbc22d0c2ad19db04ef8c50d1969689b4542bca91c7383e99eadfbca3bc38dab7e8d77e987cc017d051744fe7b29a8b0926b2fe7b4a667b2
languageName: node
linkType: hard
"daterangepicker@npm:2.1.27":
version: 2.1.27
resolution: "daterangepicker@npm:2.1.27"