mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
[web] Remove SubscriptionFormatters, format subscription dates in the frontend (#36409)
* [web] Remove SubscriptionFormatters, format subscription dates in the frontend Replace the server-formatted nextPaymentDueAt/nextPaymentDueDate/ trialEndsAtFormatted fields with the raw periodEnd date and format in the React dashboard (same strings as before, English + UTC). Also consolidate hand-rolled trial checks onto isInFreeTrial / SubscriptionHelper.isInTrial. * [web] Remove the now-unused dateformat dependency * [web] Address review: payment-dates unit test, UTC dates in acceptance test Add a dedicated unit test asserting the exact formatted strings from formatPaymentDate/formatPaymentDateTime, and use explicit UTC timestamps in the SubscriptionDashboard acceptance fixtures instead of local-timezone Date constructors. GitOrigin-RevId: 16ac4f1098b14ef9f1478d22cf39c1357ead7dea
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
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,7 +5,6 @@ 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'
|
||||
@@ -257,9 +256,9 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
|
||||
}
|
||||
})
|
||||
const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
|
||||
const isInTrial =
|
||||
paymentRecord.subscription.trialPeriodEnd &&
|
||||
paymentRecord.subscription.trialPeriodEnd.getTime() > Date.now()
|
||||
const isInTrial = SubscriptionHelper.isInTrial(
|
||||
paymentRecord.subscription.trialPeriodEnd
|
||||
)
|
||||
|
||||
let isEligibleForPause = false
|
||||
const commonPauseConditions =
|
||||
@@ -302,18 +301,10 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
|
||||
additionalLicenses,
|
||||
addOns,
|
||||
totalLicenses,
|
||||
nextPaymentDueAt: SubscriptionFormatters.formatDateTime(
|
||||
paymentRecord.subscription.periodEnd
|
||||
),
|
||||
nextPaymentDueDate: SubscriptionFormatters.formatDate(
|
||||
paymentRecord.subscription.periodEnd
|
||||
),
|
||||
periodEnd: 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,6 +17,7 @@ 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]
|
||||
|
||||
@@ -39,10 +40,7 @@ export default function PauseSubscriptionModal() {
|
||||
const subscription = personalSubscription as PaidSubscription
|
||||
eventTracking.sendMB('subscription-page-cancel-button-click', {
|
||||
plan_code: subscription?.planCode,
|
||||
is_trial:
|
||||
subscription?.payment.trialEndsAtFormatted &&
|
||||
subscription?.payment.trialEndsAt &&
|
||||
new Date(subscription.payment.trialEndsAt).getTime() > Date.now(),
|
||||
is_trial: isInFreeTrial(subscription?.payment.trialEndsAt),
|
||||
})
|
||||
setShowCancellation(true)
|
||||
}
|
||||
|
||||
+14
-9
@@ -30,6 +30,10 @@ 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,
|
||||
@@ -138,7 +142,7 @@ export function ActiveSubscription({
|
||||
<p className="mb-1" data-testid="renews-on">
|
||||
<Trans
|
||||
i18nKey="renews_on"
|
||||
values={{ date: subscription.payment.nextPaymentDueDate }}
|
||||
values={{ date: formatPaymentDate(subscription.payment.periodEnd) }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
@@ -198,13 +202,12 @@ 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) &&
|
||||
subscription.payment.trialEndsAtFormatted && (
|
||||
<TrialEnding
|
||||
trialEndsAtFormatted={subscription.payment.trialEndsAtFormatted}
|
||||
className="mb-1"
|
||||
/>
|
||||
)}
|
||||
{isInFreeTrial(subscription.payment.trialEndsAt) && (
|
||||
<TrialEnding
|
||||
trialEndsAt={subscription.payment.trialEndsAt}
|
||||
className="mb-1"
|
||||
/>
|
||||
)}
|
||||
{subscription.payment.totalLicenses > 0 && (
|
||||
<p className="mb-1" data-testid="plan-licenses">
|
||||
{isLegacyPlan &&
|
||||
@@ -245,7 +248,9 @@ export function ActiveSubscription({
|
||||
i18nKey="your_subscription_will_pause_on"
|
||||
values={{
|
||||
planName: subscription.plan.name,
|
||||
pauseDate: subscription.payment.nextPaymentDueAt,
|
||||
pauseDate: formatPaymentDateTime(
|
||||
subscription.payment.periodEnd
|
||||
),
|
||||
reactivationDate: getFormattedRenewalDate(),
|
||||
}}
|
||||
shouldUnescape
|
||||
|
||||
+2
-1
@@ -22,6 +22,7 @@ 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
|
||||
@@ -189,7 +190,7 @@ function AddOns({
|
||||
)
|
||||
}
|
||||
displayPrice={addOnsDisplayPrices[addOn.addOnCode]}
|
||||
nextBillingDate={subscription.payment.nextPaymentDueDate}
|
||||
nextBillingDate={formatPaymentDate(subscription.payment.periodEnd)!}
|
||||
/>
|
||||
))}
|
||||
{hasAiAssistViaWritefull && <WritefullManagedBundleAddOn />}
|
||||
|
||||
+2
-4
@@ -6,6 +6,7 @@ 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()
|
||||
@@ -18,10 +19,7 @@ export function CancelSubscriptionButton() {
|
||||
} = useSubscriptionDashboardContext()
|
||||
|
||||
const subscription = personalSubscription as PaidSubscription
|
||||
const isInTrial =
|
||||
subscription?.payment.trialEndsAtFormatted &&
|
||||
subscription?.payment.trialEndsAt &&
|
||||
new Date(subscription.payment.trialEndsAt).getTime() > Date.now()
|
||||
const isInTrial = isInFreeTrial(subscription?.payment.trialEndsAt)
|
||||
const hasPendingOrActivePause =
|
||||
subscription.payment.state === 'paused' ||
|
||||
(subscription.payment.state === 'active' &&
|
||||
|
||||
+4
-1
@@ -4,6 +4,7 @@ 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'
|
||||
|
||||
@@ -36,7 +37,9 @@ export function FlashMessage() {
|
||||
<Trans
|
||||
i18nKey="your_subscription_will_pause_on_short"
|
||||
values={{
|
||||
pauseDate: subscription.payment.nextPaymentDueAt,
|
||||
pauseDate: formatPaymentDateTime(
|
||||
subscription.payment.periodEnd
|
||||
),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
|
||||
+8
-6
@@ -1,5 +1,10 @@
|
||||
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
|
||||
@@ -10,14 +15,11 @@ function SubscriptionRemainder({
|
||||
subscription,
|
||||
hideTime,
|
||||
}: SubscriptionRemainderProps) {
|
||||
const stillInATrial =
|
||||
subscription.payment.trialEndsAtFormatted &&
|
||||
subscription.payment.trialEndsAt &&
|
||||
new Date(subscription.payment.trialEndsAt).getTime() > Date.now()
|
||||
const stillInATrial = isInFreeTrial(subscription.payment.trialEndsAt)
|
||||
|
||||
const terminationDate = hideTime
|
||||
? subscription.payment.nextPaymentDueDate
|
||||
: subscription.payment.nextPaymentDueAt
|
||||
? formatPaymentDate(subscription.payment.periodEnd)
|
||||
: formatPaymentDateTime(subscription.payment.periodEnd)
|
||||
return stillInATrial ? (
|
||||
<Trans
|
||||
i18nKey="subscription_will_remain_active_until_end_of_trial_period_x"
|
||||
|
||||
+4
-6
@@ -1,19 +1,17 @@
|
||||
import { Trans } from 'react-i18next'
|
||||
import { formatPaymentDateTime } from '../../../../util/payment-dates'
|
||||
|
||||
type TrialEndingProps = {
|
||||
trialEndsAtFormatted: string
|
||||
trialEndsAt: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TrialEnding({
|
||||
trialEndsAtFormatted,
|
||||
className,
|
||||
}: TrialEndingProps) {
|
||||
export function TrialEnding({ trialEndsAt, className }: TrialEndingProps) {
|
||||
return (
|
||||
<p className={className} data-testid="trial-ending">
|
||||
<Trans
|
||||
i18nKey="youre_on_free_trial_which_ends_on"
|
||||
values={{ date: trialEndsAtFormatted }}
|
||||
values={{ date: formatPaymentDateTime(trialEndsAt) }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
|
||||
+4
-1
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -48,7 +49,9 @@ export function CanceledSubscription({
|
||||
<Trans
|
||||
i18nKey="subscription_canceled_and_terminate_on_x"
|
||||
values={{
|
||||
terminateDate: subscription.payment.nextPaymentDueAt,
|
||||
terminateDate: formatPaymentDateTime(
|
||||
subscription.payment.periodEnd
|
||||
),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@ 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()
|
||||
@@ -42,7 +43,7 @@ function SubscriptionDashboard() {
|
||||
subscription &&
|
||||
subscription.pendingPlan &&
|
||||
subscription.pendingPlan.name !== subscription.plan.name
|
||||
const nextPaymentDueDate = subscription?.payment?.nextPaymentDueDate
|
||||
const nextPaymentDueDate = formatPaymentDate(subscription?.payment?.periodEnd)
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
|
||||
+7
-3
@@ -15,6 +15,7 @@ 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()
|
||||
@@ -56,8 +57,9 @@ function SuccessfulSubscription() {
|
||||
i18nKey="next_payment_of_x_collectected_on_y"
|
||||
values={{
|
||||
paymentAmmount: subscription.payment.displayPrice,
|
||||
collectionDate:
|
||||
subscription.payment.nextPaymentDueAt,
|
||||
collectionDate: formatPaymentDateTime(
|
||||
subscription.payment.periodEnd
|
||||
),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
@@ -175,7 +177,9 @@ function UpgradeSuccess({
|
||||
i18nKey="next_payment_of_x_collectected_on_y"
|
||||
values={{
|
||||
paymentAmmount: subscription.payment.displayPrice,
|
||||
collectionDate: subscription.payment.nextPaymentDueAt,
|
||||
collectionDate: formatPaymentDateTime(
|
||||
subscription.payment.periodEnd
|
||||
),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
|
||||
+2
-1
@@ -33,6 +33,7 @@ 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
|
||||
@@ -150,7 +151,7 @@ export function SubscriptionDashboardProvider({
|
||||
!personalSubscription.payment.pausedAt ||
|
||||
!personalSubscription.payment.remainingPauseCycles
|
||||
) {
|
||||
return personalSubscription.payment.nextPaymentDueAt
|
||||
return formatPaymentDateTime(personalSubscription.payment.periodEnd)!
|
||||
}
|
||||
const pausedDate = new Date(personalSubscription.payment.pausedAt)
|
||||
pausedDate.setMonth(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export default function isInFreeTrial(trialEndsAt?: string | null) {
|
||||
export default function isInFreeTrial(
|
||||
trialEndsAt?: string | null
|
||||
): trialEndsAt is string {
|
||||
if (!trialEndsAt) return false
|
||||
|
||||
const endDate = new Date(trialEndsAt)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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)
|
||||
}
|
||||
@@ -130,7 +130,6 @@
|
||||
"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",
|
||||
@@ -264,7 +263,6 @@
|
||||
"@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",
|
||||
|
||||
+7
-3
@@ -22,6 +22,7 @@ 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 () {
|
||||
@@ -77,9 +78,12 @@ describe('<PersonalSubscription />', function () {
|
||||
'Your subscription has been canceled and will terminate on',
|
||||
{ exact: false }
|
||||
)
|
||||
screen.getByText(canceledSubscription.payment!.nextPaymentDueAt, {
|
||||
exact: false,
|
||||
})
|
||||
screen.getByText(
|
||||
formatPaymentDateTime(canceledSubscription.payment!.periodEnd)!,
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
)
|
||||
|
||||
screen.getByText('No further payments will be taken.', { exact: false })
|
||||
|
||||
|
||||
+11
-4
@@ -27,6 +27,10 @@ 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
|
||||
@@ -75,7 +79,7 @@ describe('<ActiveSubscription />', function () {
|
||||
within(screen.getByTestId('renews-on')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`Renews on ${subscription.payment.nextPaymentDueDate}`
|
||||
`Renews on ${formatPaymentDate(subscription.payment.periodEnd)}`
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -210,7 +214,7 @@ describe('<ActiveSubscription />', function () {
|
||||
within(screen.getByTestId('trial-ending')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`You’re on a free trial which ends on ${trialSubscription.payment.trialEndsAtFormatted}`
|
||||
`You’re on a free trial which ends on ${formatPaymentDateTime(trialSubscription.payment.trialEndsAt)}`
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -288,7 +292,7 @@ describe('<ActiveSubscription />', function () {
|
||||
{ exact: false }
|
||||
)
|
||||
const dates = screen.getAllByText(
|
||||
annualActiveSubscription.payment.nextPaymentDueAt,
|
||||
formatPaymentDate(annualActiveSubscription.payment.periodEnd)!,
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
@@ -307,7 +311,10 @@ describe('<ActiveSubscription />', function () {
|
||||
{ exact: false }
|
||||
)
|
||||
const dates = screen.getAllByText(
|
||||
trialSubscription.payment.trialEndsAtFormatted!
|
||||
formatPaymentDate(trialSubscription.payment.trialEndsAt)!,
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
)
|
||||
expect(dates.length).to.equal(3)
|
||||
const button = screen.getByRole('button', {
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ 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 () {
|
||||
@@ -122,7 +123,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 ${annualActiveSubscriptionPro.payment.nextPaymentDueAt}.`
|
||||
`The next payment of ${annualActiveSubscriptionPro.payment.displayPrice} will be collected on ${formatPaymentDateTime(annualActiveSubscriptionPro.payment.periodEnd)}.`
|
||||
)
|
||||
screen.getByText(/taxes may be added, depending on your billing address/i)
|
||||
|
||||
|
||||
@@ -3,17 +3,11 @@ 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 nextPaymentDueAt = dateformat(oneYearFromToday, 'dS mmmm yyyy')
|
||||
const nextPaymentDueDate = dateformat(oneYearFromToday, 'dS mmmm yyyy')
|
||||
const periodEnd = new Date(oneYearFromToday).toISOString()
|
||||
const sevenDaysFromToday = new Date().setDate(today.getDate() + 7)
|
||||
const sevenDaysFromTodayFormatted = dateformat(
|
||||
sevenDaysFromToday,
|
||||
'dS mmmm yyyy'
|
||||
)
|
||||
const sevenDaysFromTodayISO = new Date(sevenDaysFromToday).toISOString()
|
||||
|
||||
export const annualActiveSubscription: PaidSubscription = {
|
||||
manager_ids: ['abc123'],
|
||||
@@ -39,11 +33,9 @@ export const annualActiveSubscription: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -84,11 +76,9 @@ export const pastDueActiveSubscription: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'past_due',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -129,11 +119,9 @@ export const annualActiveSubscriptionWithCoupons: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [
|
||||
{ name: 'Coupon1', code: 'c1', description: 'Coupon1 for 10% off' },
|
||||
@@ -177,12 +165,10 @@ export const pendingPausedSubscription: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
remainingPauseCycles: 1,
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -223,12 +209,10 @@ export const pausedSubscription: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'paused',
|
||||
remainingPauseCycles: 1,
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -269,11 +253,9 @@ export const annualActiveSubscriptionWithAddons: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -317,11 +299,9 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'EUR',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -361,11 +341,9 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -406,11 +384,9 @@ export const pastDueExpiredSubscription: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'expired',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -451,11 +427,9 @@ export const canceledSubscription: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'canceled',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -496,11 +470,9 @@ export const pendingAddOnChange: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -555,11 +527,9 @@ export const pendingSubscriptionChange: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -611,11 +581,9 @@ export const groupActiveSubscription: GroupSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 10,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -660,11 +628,9 @@ export const groupProfessionalActiveSubscription: GroupSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 10,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -710,11 +676,9 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 11,
|
||||
totalLicenses: 21,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
@@ -766,12 +730,10 @@ export const trialSubscription: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt: sevenDaysFromTodayFormatted,
|
||||
nextPaymentDueDate: sevenDaysFromTodayFormatted,
|
||||
periodEnd: sevenDaysFromTodayISO,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: sevenDaysFromTodayFormatted,
|
||||
trialEndsAt: new Date(sevenDaysFromToday).toString(),
|
||||
trialEndsAt: sevenDaysFromTodayISO,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
@@ -831,12 +793,10 @@ export const trialCollaboratorSubscription: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt: sevenDaysFromTodayFormatted,
|
||||
nextPaymentDueDate: sevenDaysFromTodayFormatted,
|
||||
periodEnd: sevenDaysFromTodayISO,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: sevenDaysFromTodayFormatted,
|
||||
trialEndsAt: new Date(sevenDaysFromToday).toString(),
|
||||
trialEndsAt: sevenDaysFromTodayISO,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'foo@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
@@ -875,11 +835,9 @@ export const monthlyActiveCollaborator: PaidSubscription = {
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
periodEnd,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'foo@example.com',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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 () {
|
||||
@@ -15,10 +14,6 @@ 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)
|
||||
const sevenDaysFromTodayFormatted = dateformat(
|
||||
sevenDaysFromToday,
|
||||
'dS mmmm yyyy'
|
||||
)
|
||||
expect(isInFreeTrial(sevenDaysFromTodayFormatted)).to.be.true
|
||||
expect(isInFreeTrial(new Date(sevenDaysFromToday).toISOString())).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
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,12 +745,10 @@ describe('SubscriptionViewModelBuilder', function () {
|
||||
},
|
||||
],
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt: 'February 20th, 2025 12:00 PM UTC',
|
||||
nextPaymentDueDate: 'February 20th, 2025',
|
||||
periodEnd: new Date('2025-02-20T12:00:00.000Z'),
|
||||
currency: 'EUR',
|
||||
planPrice: 13,
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'example@example.com',
|
||||
|
||||
@@ -37,12 +37,10 @@ type PaymentProviderRecord = {
|
||||
additionalLicenses: number
|
||||
addOns: PaymentProviderAddOn[]
|
||||
totalLicenses: number
|
||||
nextPaymentDueAt: string
|
||||
nextPaymentDueDate: string
|
||||
periodEnd: string
|
||||
currency: CurrencyCode
|
||||
planPrice: number
|
||||
state?: SubscriptionState
|
||||
trialEndsAtFormatted: Nullable<string>
|
||||
trialEndsAt: Nullable<string>
|
||||
activeCoupons: PaymentProviderCoupon[]
|
||||
accountEmail: string
|
||||
|
||||
@@ -8006,7 +8006,6 @@ __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"
|
||||
@@ -8100,7 +8099,6 @@ __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"
|
||||
@@ -10795,13 +10793,6 @@ __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"
|
||||
@@ -16431,13 +16422,6 @@ __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"
|
||||
|
||||
Reference in New Issue
Block a user