Skip to content

Commit

Permalink
feat: charge paid in advance
Browse files Browse the repository at this point in the history
  • Loading branch information
ansmonjol committed Jul 11, 2023
1 parent b988974 commit 8c56a3a
Show file tree
Hide file tree
Showing 15 changed files with 619 additions and 214 deletions.
6 changes: 3 additions & 3 deletions cypress/e2e/t3-create-bm.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ describe('Create billable metrics', () => {
cy.get('textarea[name="description"]').type('I am a description')
cy.get('[data-test="submit"]').should('be.disabled')
cy.get('input[name="aggregationType"]').click()
cy.get('[data-test="recurring_count_agg"]').click()
cy.get('[data-test="submit"]').should('be.disabled')
cy.get('input[name="fieldName"]').type('whatever')
cy.get('[data-test="count_agg"]').click()
cy.get('input[name="fieldName"]').should('not.exist')
cy.get('[data-test="button-selector-true"]').click()
cy.get('[data-test="submit"]').should('not.be.disabled')
cy.get('[data-test="submit"]').click()
cy.url().should('be.equal', Cypress.config().baseUrl + '/billable-metrics')
Expand Down
12 changes: 12 additions & 0 deletions cypress/e2e/t4-create-plan.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ describe('Create plan', () => {

// Standard
cy.get('[data-test="add-charge"]').first().click()
cy.get('[data-test="add-metered-charge"]').first().click()
cy.get('input[name="searchMeteredChargeInput"]').click()
cy.get('[data-option-index="0"]').click()
cy.get('[data-test="remove-charge"]').should('exist').and('not.be.disabled')
cy.get('input[name="chargeModel"]').last().should('have.value', 'Standard pricing')
Expand All @@ -51,6 +53,8 @@ describe('Create plan', () => {

// Graduated
cy.get('[data-test="add-charge"]').last().click()
cy.get('[data-test="add-metered-charge"]').last().click()
cy.get('input[name="searchMeteredChargeInput"]').click()
cy.get('[data-option-index="1"]').click()
cy.get('[data-test="remove-charge"]').should('exist').and('not.be.disabled')
cy.get('input[name="chargeModel"]').last().click()
Expand All @@ -66,6 +70,8 @@ describe('Create plan', () => {

// Package
cy.get('[data-test="add-charge"]').last().click()
cy.get('[data-test="add-metered-charge"]').last().click()
cy.get('input[name="searchMeteredChargeInput"]').click()
cy.get('[data-option-index="1"]').click()
cy.get('[data-test="remove-charge"]').should('exist').and('not.be.disabled')
cy.get('input[name="chargeModel"]').last().click()
Expand All @@ -76,6 +82,8 @@ describe('Create plan', () => {

// Percentage
cy.get('[data-test="add-charge"]').last().click()
cy.get('[data-test="add-metered-charge"]').last().click()
cy.get('input[name="searchMeteredChargeInput"]').click()
cy.get('[data-option-index="1"]').click()
cy.get('[data-test="remove-charge"]').should('exist').and('not.be.disabled')
cy.get('input[name="chargeModel"]').last().click()
Expand All @@ -94,6 +102,8 @@ describe('Create plan', () => {

// Volume
cy.get('[data-test="add-charge"]').last().click()
cy.get('[data-test="add-recurring-charge"]').last().click()
cy.get('input[name="searchRecurringChargeInput"]').click()
cy.get('[data-option-index="1"]').click()
cy.get('[data-test="remove-charge"]').should('exist').and('not.be.disabled')
cy.get('input[name="chargeModel"]').last().click()
Expand Down Expand Up @@ -127,6 +137,8 @@ describe('Create plan', () => {

// Config charge
cy.get('[data-test="add-charge"]').last().click()
cy.get('[data-test="add-metered-charge"]').last().click()
cy.get('input[name="searchMeteredChargeInput"]').click()
cy.get('[data-option-index="1"]').click()
cy.get('[data-test="remove-charge"]').should('exist').and('not.be.disabled')
cy.get('input[name="chargeModel"]').last().click()
Expand Down
25 changes: 19 additions & 6 deletions ditto/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,25 @@
"text_646e2d0cc536351b62ba6f25": "Generate an invoice for each event",
"text_646e2d0cc536351b62ba6f35": "Turn it off to avoid generating an invoice for each event.",
"text_646e2d0cc536351b62ba6efd": "Charges are invoiced at the end of the billing period and cannot be charged in advance due to their aggregation type being defined as recurring count.",
"text_648c2be974f70300748a4ca1": "Type",
"text_648c2be974f70300748a4ca5": "Metered",
"text_648c2be974f70300748a4cad": "The calculated value is reset to 0 at the beginning of each period",
"text_648c2be974f70300748a4c87": "Recurring",
"text_648c2be974f70300748a4c8c": "The calculated value is not reset to 0, it is persistent over all billable periods",
"text_648c2be974f70300748a4ca6": "This billable metric is metered, the calculated value is reset to 0 at the beginning of each period",
"text_648c2be974f70300748a4cc6": "Add a metered charge",
"text_648c2be974f70300748a4cc8": "Add a recurring charge",
"text_648c2be974f70300748a4cc7": "Metered charges",
"text_648c2be974f70300748a4cc9": "Charges are fully billed, invoiced and reset at the end of each billing period.",
"text_648c2be974f70300748a4cce": "Add a metered charge",
"text_648c2be974f70300748a4ccf": "Recurring charges",
"text_648c2be974f70300748a4cd0": "Charges persist across billing periods and can be billed and invoiced either upon event receipt or at the end of the billing period.",
"text_648c2be974f70300748a4cd4": "Add a recurring charge",
"text_649c54823c90890062476255": "Prorate charge amount",
"text_649c47c0a6c1f200de8ff48d": "Prorated charge",
"text_649c54823c9089006247625a": "Can’t be prorated due to {{chargeModel}} pricing model defined in above.",
"text_649c49bcebd91c0082d84446": "Full charge",
"text_649c54823c90890062476259": "Based between the event reception date and the end date of the period",
"text_645bb193927b375079d28a8f": "Taxes",
"text_645bb193927b375079d28ab5": "Taxes settings",
"text_645bb193927b375079d28ad2": "Add",
Expand Down Expand Up @@ -984,12 +1003,6 @@
"text_645d071272418a14c1c76b25": "Adyen connection successfully deleted",
"text_645d0728ea0a5a7bbf76d5c7": "Create automatically this customer in Adyen",
"text_645d0728ea0a5a7bbf76d5c9": "To ensure customer creation, their external ID must contain a minimum of 3 characters, and the payment registration must be completed using the checkout URL sent via webhook.",
"text_6310755befed49627644222b": "Metered",
"text_6310755befed49627644222d": "• The calculated value is reset to 0 at the beginning of each period",
"text_6310755befed49627644222f": "Persistent",
"text_6310755befed496276442231": "• The calculated value is not reset to 0, it persists over all billing periods",
"text_63105dbdd88c7432a3b255eb": "Recurring count",
"text_6435888d7cc86500646d897a": "Add a charge",
"text_64639c4d172d7a006ef30516": "Create a tax_rate",
"text_64639c4d172d7a006ef30514": "Tax rate",
"text_64639c4d172d7a006ef30515": "Search or select a tax rate",
Expand Down
32 changes: 4 additions & 28 deletions src/components/billableMetrics/BillableMetricCodeSnippet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { apiUrl } = envGlobalVar()
const getSnippets = (billableMetric?: CreateBillableMetricInput) => {
if (!billableMetric) return '# Fill the form to generate the code snippet'

const { aggregationType, code, fieldName, group } = billableMetric
const { aggregationType, code, fieldName, group, recurring } = billableMetric

const isValidJSON = (string: string) => {
try {
Expand Down Expand Up @@ -70,6 +70,7 @@ const getSnippets = (billableMetric?: CreateBillableMetricInput) => {
"external_subscription_id": "__EXTERNAL_SUBSCRIPTION_ID__",
"external_customer_id": "__EXTERNAL_CUSTOMER_ID__",
"code": "${code}",
"recurring": "${recurring}",
"timestamp": $(date +%s)${
groupDimension > 0
? `,
Expand Down Expand Up @@ -120,7 +121,8 @@ ${groupDimensionMessage}
"transaction_id": "__UNIQUE_ID__",
"external_subscription_id": "__EXTERNAL_SUBSCRIPTION_ID__",
"external_customer_id": "__EXTERNAL_CUSTOMER_ID__",
"code": "${code}",
"code": "${code}",
"recurring": "${recurring}",
"timestamp": $(date +%s),
"properties": {
"${fieldName}": 12${
Expand All @@ -135,32 +137,6 @@ ${groupDimensionMessage}
# To use the snippet, don’t forget to edit your __YOUR_API_KEY__, __UNIQUE_ID__, __EXTERNAL_SUBSCRIPTION_ID__ and __EXTERNAL_CUSTOMER_ID__
${groupDimensionMessage}
`
case AggregationTypeEnum.RecurringCountAgg:
return `curl --location --request POST "${apiUrl}/api/v1/events" \\
--header "Authorization: Bearer $__YOUR_API_KEY__" \\
--header 'Content-Type: application/json' \\
--data-raw '{
"event": {
"transaction_id": "__UNIQUE_ID__",
"external_subscription_id": "__EXTERNAL_SUBSCRIPTION_ID__",
"external_customer_id": "__EXTERNAL_CUSTOMER_ID__",
"code": "${code}",
"timestamp": $(date +%s),
"properties": {
"${fieldName}": "__VALUE__" ,
"operation_type": "add"${
groupDimension > 0
? `,
${propertiesForGroup}`
: ''
}
}
}
}'
# To use the snippet, don’t forget to edit your __YOUR_API_KEY__, __UNIQUE_ID__, __EXTERNAL_SUBSCRIPTION_ID__, __EXTERNAL_CUSTOMER_ID__ and __VALUE__
${groupDimensionMessage}
`
default:
return '# Fill the form to generate the code snippet'
Expand Down
1 change: 1 addition & 0 deletions src/components/form/ButtonSelector/ButtonSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const ButtonSelector = ({
title={optionLabel ?? optionValue}
active={value === optionValue}
onClick={() => onChange(optionValue)}
data-test={`button-selector-${optionValue}`}
/>
)
})}
Expand Down
6 changes: 5 additions & 1 deletion src/components/form/ButtonSelector/TabButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export interface TabButtonProps {
}

export const TabButton = forwardRef<HTMLButtonElement, TabButtonProps>(
({ active = false, title, icon, className, disabled, onClick }: TabButtonProps, ref) => {
(
{ active = false, title, icon, className, disabled, onClick, ...props }: TabButtonProps,
ref
) => {
const [isLoading, setIsLoading] = useState(false)
const mountedRef = useRef(false)

Expand All @@ -32,6 +35,7 @@ export const TabButton = forwardRef<HTMLButtonElement, TabButtonProps>(

return (
<Container
{...props}
ref={ref}
className={className}
$active={active}
Expand Down
95 changes: 74 additions & 21 deletions src/components/plans/ChargeAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ gql`
invoiceable
minAmountCents
payInAdvance
prorated
properties {
amount
}
Expand All @@ -74,6 +75,7 @@ gql`
name
code
aggregationType
recurring
flatGroups {
id
key
Expand Down Expand Up @@ -138,13 +140,24 @@ export const ChargeAccordion = memo(

const handleUpdate = useCallback(
(name: string, value: string | boolean) => {
if (
name === 'chargeModel' &&
(value === ChargeModelEnum.Volume ||
if (name === 'chargeModel') {
// Reset pay in advance when switching charge model
if (
value === ChargeModelEnum.Volume ||
localCharge.billableMetric.aggregationType === AggregationTypeEnum.MaxAgg ||
localCharge.billableMetric.aggregationType === AggregationTypeEnum.RecurringCountAgg)
) {
formikProps.setFieldValue(`charges.${index}.payInAdvance`, false)
localCharge.billableMetric.aggregationType === AggregationTypeEnum.RecurringCountAgg
) {
formikProps.setFieldValue(`charges.${index}.payInAdvance`, false)
}

// Reset prorated when switching charge model
if (
(localCharge.billableMetric.recurring && value === ChargeModelEnum.Graduated) ||
value === ChargeModelEnum.Package ||
value === ChargeModelEnum.Percentage
) {
formikProps.setFieldValue(`charges.${index}.prorated`, false)
}
}

if (name === 'payInAdvance') {
Expand All @@ -156,11 +169,15 @@ export const ChargeAccordion = memo(
formikProps.setFieldValue(`charges.${index}.invoiceable`, true)
}
}

formikProps.setFieldValue(`charges.${index}.${name}`, value)
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[index, formikProps.setFieldValue]

[
formikProps,
index,
localCharge.billableMetric.aggregationType,
localCharge.billableMetric.recurring,
]
)

const chargePayInAdvanceSwitchHelperText = useMemo(() => {
Expand All @@ -185,6 +202,21 @@ export const ChargeAccordion = memo(
translate,
])

const isProratedOptionDisabled = useMemo(() => {
return (
localCharge.chargeModel === ChargeModelEnum.Volume ||
localCharge.chargeModel === ChargeModelEnum.Package ||
localCharge.chargeModel === ChargeModelEnum.Percentage
)
}, [localCharge.chargeModel])

const proratedOptionHelperText = useMemo(() => {
if (isProratedOptionDisabled)
return translate('text_649c54823c9089006247625a', { chargeModel: localCharge.chargeModel })

return translate('text_649c54823c90890062476259')
}, [isProratedOptionDisabled, translate, localCharge.chargeModel])

return (
<Accordion
noContentMargin
Expand Down Expand Up @@ -268,18 +300,22 @@ export const ChargeAccordion = memo(
label: translate('text_624aa732d6af4e0103d40e6f'),
value: ChargeModelEnum.Standard,
},
{
label: translate('text_62793bbb599f1c01522e919f'),
value: ChargeModelEnum.Graduated,
},
{
label: translate('text_62a0b7107afa2700a65ef6e2'),
value: ChargeModelEnum.Percentage,
},
{
label: translate('text_6282085b4f283b0102655868'),
value: ChargeModelEnum.Package,
},
...(!localCharge.billableMetric.recurring
? [
{
label: translate('text_62793bbb599f1c01522e919f'),
value: ChargeModelEnum.Graduated,
},
{
label: translate('text_62a0b7107afa2700a65ef6e2'),
value: ChargeModelEnum.Percentage,
},
{
label: translate('text_6282085b4f283b0102655868'),
value: ChargeModelEnum.Package,
},
]
: []),
{
label: translate('text_6304e74aab6dbc18d615f386'),
value: ChargeModelEnum.Volume,
Expand Down Expand Up @@ -394,6 +430,23 @@ export const ChargeAccordion = memo(
},
]}
/>

{!!localCharge.billableMetric.recurring && (
<Switch
name={`charge-${localCharge.id}-prorated`}
label={translate('text_649c54823c90890062476255')}
disabled={
disabled ||
(localCharge.billableMetric.recurring &&
localCharge.chargeModel === ChargeModelEnum.Graduated) ||
localCharge.chargeModel === ChargeModelEnum.Package ||
localCharge.chargeModel === ChargeModelEnum.Percentage
}
subLabel={proratedOptionHelperText}
checked={!!localCharge.prorated}
onChange={(value) => handleUpdate('prorated', Boolean(value))}
/>
)}
{localCharge.payInAdvance && (
<InvoiceableSwitchWrapper>
<Switch
Expand Down
9 changes: 9 additions & 0 deletions src/components/plans/ChargeOptionsAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ export const ChargeOptionsAccordion = ({
: translate('text_646e2d0cc536351b62ba6f0c')
}
/>
{!!charge.billableMetric.recurring && (
<Chip
label={
charge.prorated
? translate('text_649c47c0a6c1f200de8ff48d')
: translate('text_649c49bcebd91c0082d84446')
}
/>
)}
<Chip
label={
charge.invoiceable
Expand Down
Loading

0 comments on commit 8c56a3a

Please sign in to comment.