Make territory billing period changeable (#840)
* allow updates to territory billing * simplify prorating * handle updates during grace period and rehydrating archive
This commit is contained in:
parent
cfd762a5b6
commit
798fab097d
|
@ -1,9 +1,9 @@
|
|||
import { GraphQLError } from 'graphql'
|
||||
import { serializeInvoicable } from './serial'
|
||||
import { TERRITORY_COST_MONTHLY, TERRITORY_COST_ONCE, TERRITORY_COST_YEARLY } from '../../lib/constants'
|
||||
import { TERRITORY_COST_MONTHLY, TERRITORY_COST_ONCE, TERRITORY_COST_YEARLY, TERRITORY_PERIOD_COST } from '../../lib/constants'
|
||||
import { datePivot, whenRange } from '../../lib/time'
|
||||
import { ssValidate, territorySchema } from '../../lib/validate'
|
||||
import { nextBilling, nextNextBilling } from '../../lib/territory'
|
||||
import { nextBilling, proratedBillingCost } from '../../lib/territory'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||
import { subViewGroup } from './growth'
|
||||
|
||||
|
@ -12,9 +12,20 @@ export function paySubQueries (sub, models) {
|
|||
return []
|
||||
}
|
||||
|
||||
const billingAt = nextBilling(sub)
|
||||
const billAt = nextNextBilling(sub)
|
||||
const cost = BigInt(sub.billingCost) * BigInt(1000)
|
||||
// if in active or grace, consider we are billing them from where they are paid up
|
||||
// and use grandfathered cost
|
||||
let billedLastAt = sub.billPaidUntil
|
||||
let billingCost = sub.billingCost
|
||||
|
||||
// if the sub is archived, they are paying to reactivate it
|
||||
if (sub.status === 'STOPPED') {
|
||||
// get non-grandfathered cost and reset their billing to start now
|
||||
billedLastAt = new Date()
|
||||
billingCost = TERRITORY_PERIOD_COST(sub.billingType)
|
||||
}
|
||||
|
||||
const billPaidUntil = nextBilling(billedLastAt, sub.billingType)
|
||||
const cost = BigInt(billingCost) * BigInt(1000)
|
||||
|
||||
return [
|
||||
models.user.update({
|
||||
|
@ -33,7 +44,9 @@ export function paySubQueries (sub, models) {
|
|||
name: sub.name
|
||||
},
|
||||
data: {
|
||||
billedLastAt: billingAt,
|
||||
billedLastAt,
|
||||
billPaidUntil,
|
||||
billingCost,
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
}),
|
||||
|
@ -45,18 +58,7 @@ export function paySubQueries (sub, models) {
|
|||
msats: cost,
|
||||
type: 'BILLING'
|
||||
}
|
||||
}),
|
||||
models.$executeRaw`
|
||||
DELETE FROM pgboss.job
|
||||
WHERE name = 'territoryBilling'
|
||||
AND data->>'subName' = ${sub.name}
|
||||
AND completedon IS NULL`,
|
||||
// schedule 'em
|
||||
models.$queryRaw`
|
||||
INSERT INTO pgboss.job (name, data, startafter, keepuntil) VALUES ('territoryBilling',
|
||||
${JSON.stringify({
|
||||
subName: sub.name
|
||||
})}::JSONB, ${billAt}, ${datePivot(billAt, { days: 1 })})`
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -241,16 +243,16 @@ export default {
|
|||
}
|
||||
|
||||
async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
|
||||
const { billingType, nsfw } = data
|
||||
const { billingType } = data
|
||||
let billingCost = TERRITORY_COST_MONTHLY
|
||||
let billAt = datePivot(new Date(), { months: 1 })
|
||||
let billPaidUntil = datePivot(new Date(), { months: 1 })
|
||||
|
||||
if (billingType === 'ONCE') {
|
||||
billingCost = TERRITORY_COST_ONCE
|
||||
billAt = null
|
||||
billPaidUntil = null
|
||||
} else if (billingType === 'YEARLY') {
|
||||
billingCost = TERRITORY_COST_YEARLY
|
||||
billAt = datePivot(new Date(), { years: 1 })
|
||||
billPaidUntil = datePivot(new Date(), { years: 1 })
|
||||
}
|
||||
|
||||
const cost = BigInt(1000) * BigInt(billingCost)
|
||||
|
@ -272,10 +274,10 @@ async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
|
|||
models.sub.create({
|
||||
data: {
|
||||
...data,
|
||||
billPaidUntil,
|
||||
billingCost,
|
||||
rankingType: 'WOT',
|
||||
userId: me.id,
|
||||
nsfw
|
||||
userId: me.id
|
||||
}
|
||||
}),
|
||||
// record 'em
|
||||
|
@ -286,15 +288,7 @@ async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
|
|||
msats: cost,
|
||||
type: 'BILLING'
|
||||
}
|
||||
}),
|
||||
// schedule 'em
|
||||
...(billAt
|
||||
? [models.$queryRaw`
|
||||
INSERT INTO pgboss.job (name, data, startafter, keepuntil) VALUES ('territoryBilling',
|
||||
${JSON.stringify({
|
||||
subName: data.name
|
||||
})}::JSONB, ${billAt}, ${datePivot(billAt, { days: 1 })})`]
|
||||
: [])
|
||||
})
|
||||
], { models, lnd, hash, hmac, me, enforceFee: billingCost })
|
||||
|
||||
return results[1]
|
||||
|
@ -307,26 +301,88 @@ async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
|
|||
}
|
||||
|
||||
async function updateSub (parent, { oldName, ...data }, { me, models, lnd, hash, hmac }) {
|
||||
// prevent modification of billingType
|
||||
delete data.billingType
|
||||
const oldSub = await models.sub.findUnique({
|
||||
where: {
|
||||
name: oldName,
|
||||
userId: me.id,
|
||||
// this function's logic is only valid if the sub is not stopped
|
||||
// so prevent updates to stopped subs
|
||||
status: {
|
||||
not: 'STOPPED'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!oldSub) {
|
||||
throw new GraphQLError('sub not found', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await models.$transaction([
|
||||
models.sub.update({
|
||||
data,
|
||||
where: {
|
||||
name: oldName,
|
||||
userId: me.id
|
||||
}
|
||||
}),
|
||||
models.$queryRaw`
|
||||
UPDATE pgboss.job
|
||||
SET data = ${JSON.stringify({ subName: data.name })}::JSONB
|
||||
WHERE name = 'territoryBilling'
|
||||
AND data->>'subName' = ${oldName}`
|
||||
])
|
||||
// if the cost is changing, record the new cost and update billing job
|
||||
if (oldSub.billingType !== data.billingType) {
|
||||
// make sure the current cost is recorded so they are grandfathered in
|
||||
data.billingCost = TERRITORY_PERIOD_COST(data.billingType)
|
||||
|
||||
return results[0]
|
||||
// we never want to bill them again if they are changing to ONCE
|
||||
if (data.billingType === 'ONCE') {
|
||||
data.billPaidUntil = null
|
||||
data.billingAutoRenew = false
|
||||
}
|
||||
|
||||
// if they are changing to YEARLY, bill them in a year
|
||||
// if they are changing to MONTHLY from YEARLY, do nothing
|
||||
if (oldSub.billingType === 'MONTHLY' && data.billingType === 'YEARLY') {
|
||||
data.billPaidUntil = datePivot(new Date(oldSub.billedLastAt), { years: 1 })
|
||||
}
|
||||
|
||||
// if this billing change makes their bill paid up, set them to active
|
||||
if (data.billPaidUntil === null || data.billPaidUntil >= new Date()) {
|
||||
data.status = 'ACTIVE'
|
||||
}
|
||||
|
||||
// if the billing type is changing such that it's more expensive, bill 'em the difference
|
||||
const proratedCost = proratedBillingCost(oldSub, data.billingType)
|
||||
if (proratedCost > 0) {
|
||||
const cost = BigInt(1000) * BigInt(proratedCost)
|
||||
const results = await serializeInvoicable([
|
||||
models.user.update({
|
||||
where: {
|
||||
id: me.id
|
||||
},
|
||||
data: {
|
||||
msats: {
|
||||
decrement: cost
|
||||
}
|
||||
}
|
||||
}),
|
||||
models.subAct.create({
|
||||
data: {
|
||||
userId: me.id,
|
||||
subName: oldName,
|
||||
msats: cost,
|
||||
type: 'BILLING'
|
||||
}
|
||||
}),
|
||||
models.sub.update({
|
||||
data,
|
||||
where: {
|
||||
name: oldName,
|
||||
userId: me.id
|
||||
}
|
||||
})
|
||||
], { models, lnd, hash, hmac, me, enforceFee: proratedCost })
|
||||
return results[2]
|
||||
}
|
||||
}
|
||||
|
||||
// if we get here they are changin in a way that doesn't cost them anything
|
||||
return await models.sub.update({
|
||||
data,
|
||||
where: {
|
||||
name: oldName,
|
||||
userId: me.id
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.code === 'P2002') {
|
||||
throw new GraphQLError('name taken', { extensions: { code: 'BAD_INPUT' } })
|
||||
|
|
|
@ -36,6 +36,7 @@ export default gql`
|
|||
billingAutoRenew: Boolean!
|
||||
rankingType: String!
|
||||
billedLastAt: Date!
|
||||
billPaidUntil: Date
|
||||
baseCost: Int!
|
||||
status: String!
|
||||
moderated: Boolean!
|
||||
|
|
|
@ -804,7 +804,7 @@ export function Form ({
|
|||
const msg = err.message || err.toString?.()
|
||||
// handle errors from JIT invoices by ignoring them
|
||||
if (msg === 'modal closed' || msg === 'invoice canceled') return
|
||||
toaster.danger('submit error:' + msg)
|
||||
toaster.danger('submit error: ' + msg)
|
||||
}
|
||||
}, [onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix])
|
||||
|
||||
|
|
|
@ -3,12 +3,14 @@ import { Col, InputGroup, Row, Form as BootstrapForm, Badge } from 'react-bootst
|
|||
import { Checkbox, CheckboxGroup, Form, Input, MarkdownInput } from './form'
|
||||
import FeeButton, { FeeButtonProvider } from './fee-button'
|
||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { MAX_TERRITORY_DESC_LENGTH, POST_TYPES, TERRITORY_BILLING_OPTIONS } from '../lib/constants'
|
||||
import { MAX_TERRITORY_DESC_LENGTH, POST_TYPES, TERRITORY_BILLING_OPTIONS, TERRITORY_PERIOD_COST } from '../lib/constants'
|
||||
import { territorySchema } from '../lib/validate'
|
||||
import { useMe } from './me'
|
||||
import Info from './info'
|
||||
import { abbrNum } from '../lib/format'
|
||||
import { purchasedType } from '../lib/territory'
|
||||
|
||||
export default function TerritoryForm ({ sub }) {
|
||||
const router = useRouter()
|
||||
|
@ -54,9 +56,24 @@ export default function TerritoryForm ({ sub }) {
|
|||
)
|
||||
|
||||
const [billing, setBilling] = useState((sub?.billingType || 'MONTHLY').toLowerCase())
|
||||
const lineItems = useMemo(() => {
|
||||
const lines = { territory: TERRITORY_BILLING_OPTIONS('first')[billing] }
|
||||
if (!sub) return lines
|
||||
|
||||
// we are changing billing type so prorate the change
|
||||
if (sub?.billingType?.toLowerCase() !== billing) {
|
||||
const alreadyBilled = TERRITORY_PERIOD_COST(purchasedType(sub))
|
||||
lines.paid = {
|
||||
term: `- ${abbrNum(alreadyBilled)} sats`,
|
||||
label: 'already paid',
|
||||
modifier: cost => cost - alreadyBilled
|
||||
}
|
||||
return lines
|
||||
}
|
||||
}, [sub, billing])
|
||||
|
||||
return (
|
||||
<FeeButtonProvider baseLineItems={sub ? undefined : { territory: TERRITORY_BILLING_OPTIONS('first')[billing] }}>
|
||||
<FeeButtonProvider baseLineItems={lineItems}>
|
||||
<Form
|
||||
initial={{
|
||||
name: sub?.name || '',
|
||||
|
@ -74,14 +91,15 @@ export default function TerritoryForm ({ sub }) {
|
|||
onSubmit={onSubmit}
|
||||
className='mb-5'
|
||||
storageKeyPrefix={sub ? undefined : 'territory'}
|
||||
> <Input
|
||||
label='name'
|
||||
name='name'
|
||||
required
|
||||
autoFocus
|
||||
clear
|
||||
maxLength={32}
|
||||
prepend={<InputGroup.Text className='text-monospace'>~</InputGroup.Text>}
|
||||
>
|
||||
<Input
|
||||
label='name'
|
||||
name='name'
|
||||
required
|
||||
autoFocus
|
||||
clear
|
||||
maxLength={32}
|
||||
prepend={<InputGroup.Text className='text-monospace'>~</InputGroup.Text>}
|
||||
/>
|
||||
<MarkdownInput
|
||||
label='description'
|
||||
|
@ -147,51 +165,56 @@ export default function TerritoryForm ({ sub }) {
|
|||
</Col>
|
||||
</Row>
|
||||
</CheckboxGroup>
|
||||
<CheckboxGroup
|
||||
label='billing'
|
||||
name='billing'
|
||||
groupClassName='mb-0'
|
||||
>
|
||||
{(!sub?.billingType || sub.billingType === 'MONTHLY') &&
|
||||
<Checkbox
|
||||
type='radio'
|
||||
label='100k sats/month'
|
||||
value='MONTHLY'
|
||||
name='billingType'
|
||||
id='monthly-checkbox'
|
||||
readOnly={!!sub}
|
||||
handleChange={checked => checked && setBilling('monthly')}
|
||||
groupClassName='ms-1 mb-0'
|
||||
/>}
|
||||
{(!sub?.billingType || sub.billingType === 'YEARLY') &&
|
||||
<Checkbox
|
||||
type='radio'
|
||||
label='1m sats/year'
|
||||
value='YEARLY'
|
||||
name='billingType'
|
||||
id='yearly-checkbox'
|
||||
readOnly={!!sub}
|
||||
handleChange={checked => checked && setBilling('yearly')}
|
||||
groupClassName='ms-1 mb-0'
|
||||
/>}
|
||||
{(!sub?.billingType || sub.billingType === 'ONCE') &&
|
||||
<Checkbox
|
||||
type='radio'
|
||||
label='3m sats once'
|
||||
value='ONCE'
|
||||
name='billingType'
|
||||
id='once-checkbox'
|
||||
readOnly={!!sub}
|
||||
handleChange={checked => checked && setBilling('once')}
|
||||
groupClassName='ms-1 mb-0'
|
||||
/>}
|
||||
</CheckboxGroup>
|
||||
{billing !== 'once' &&
|
||||
<Checkbox
|
||||
label='auto renew'
|
||||
name='billingAutoRenew'
|
||||
groupClassName='ms-1 mt-2'
|
||||
/>}
|
||||
{sub?.billingType !== 'ONCE' &&
|
||||
<>
|
||||
<CheckboxGroup
|
||||
label={
|
||||
<span className='d-flex align-items-center'>billing
|
||||
{sub && sub.billingType !== 'ONCE' &&
|
||||
<Info>
|
||||
You will be credited what you paid for your current billing period when you change your billing period to a longer duration.
|
||||
If you change from yearly to monthly, when your year ends, your monthly billing will begin.
|
||||
</Info>}
|
||||
</span>
|
||||
}
|
||||
name='billing'
|
||||
groupClassName={billing !== 'once' ? 'mb-0' : ''}
|
||||
>
|
||||
<Checkbox
|
||||
type='radio'
|
||||
label='100k sats/month'
|
||||
value='MONTHLY'
|
||||
name='billingType'
|
||||
id='monthly-checkbox'
|
||||
handleChange={checked => checked && setBilling('monthly')}
|
||||
groupClassName='ms-1 mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
type='radio'
|
||||
label='1m sats/year'
|
||||
value='YEARLY'
|
||||
name='billingType'
|
||||
id='yearly-checkbox'
|
||||
handleChange={checked => checked && setBilling('yearly')}
|
||||
groupClassName='ms-1 mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
type='radio'
|
||||
label='3m sats once'
|
||||
value='ONCE'
|
||||
name='billingType'
|
||||
id='once-checkbox'
|
||||
handleChange={checked => checked && setBilling('once')}
|
||||
groupClassName='ms-1 mb-0'
|
||||
/>
|
||||
</CheckboxGroup>
|
||||
{billing !== 'once' &&
|
||||
<Checkbox
|
||||
label='auto-renew'
|
||||
name='billingAutoRenew'
|
||||
groupClassName='ms-1 mt-2'
|
||||
/>}
|
||||
</>}
|
||||
<AccordianItem
|
||||
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>options</div>}
|
||||
body={
|
||||
|
|
|
@ -8,7 +8,7 @@ import { LongCountdown } from './countdown'
|
|||
import { useCallback } from 'react'
|
||||
import { useApolloClient, useMutation } from '@apollo/client'
|
||||
import { SUB_PAY } from '../fragments/subs'
|
||||
import { nextBilling, nextBillingWithGrace } from '../lib/territory'
|
||||
import { nextBillingWithGrace } from '../lib/territory'
|
||||
|
||||
export default function TerritoryPaymentDue ({ sub }) {
|
||||
const me = useMe()
|
||||
|
@ -78,7 +78,7 @@ export function TerritoryBillingLine ({ sub }) {
|
|||
const me = useMe()
|
||||
if (!sub || sub.userId !== Number(me?.id)) return null
|
||||
|
||||
const dueDate = nextBilling(sub)
|
||||
const dueDate = sub.billPaidUntil && new Date(sub.billPaidUntil)
|
||||
const pastDue = dueDate && dueDate < new Date()
|
||||
|
||||
return (
|
||||
|
|
|
@ -12,6 +12,7 @@ export const SUB_FIELDS = gql`
|
|||
billingCost
|
||||
billingAutoRenew
|
||||
billedLastAt
|
||||
billPaidUntil
|
||||
baseCost
|
||||
userId
|
||||
desc
|
||||
|
|
|
@ -88,6 +88,17 @@ export const TERRITORY_BILLING_OPTIONS = (labelPrefix) => ({
|
|||
}
|
||||
})
|
||||
|
||||
export const TERRITORY_PERIOD_COST = (billingType) => {
|
||||
switch (billingType.toUpperCase()) {
|
||||
case 'MONTHLY':
|
||||
return TERRITORY_COST_MONTHLY
|
||||
case 'YEARLY':
|
||||
return TERRITORY_COST_YEARLY
|
||||
case 'ONCE':
|
||||
return TERRITORY_COST_ONCE
|
||||
}
|
||||
}
|
||||
|
||||
export const FOUND_BLURBS = [
|
||||
'The harsh frontier is no place for the unprepared. This hat will protect you from the sun, dust, and other elements Mother Nature throws your way.',
|
||||
'A cowboy is nothing without a cowboy hat. Take good care of it, and it will protect you from the sun, dust, and other elements on your journey.',
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
import { TERRITORY_GRACE_DAYS } from './constants'
|
||||
import { datePivot } from './time'
|
||||
import { TERRITORY_GRACE_DAYS, TERRITORY_PERIOD_COST } from './constants'
|
||||
import { datePivot, diffDays } from './time'
|
||||
|
||||
export function nextBilling (sub) {
|
||||
if (!sub || sub.billingType === 'ONCE') return null
|
||||
export function nextBilling (relativeTo, billingType) {
|
||||
if (!relativeTo || billingType === 'ONCE') return null
|
||||
|
||||
const pivot = sub.billingType === 'MONTHLY'
|
||||
const pivot = billingType === 'MONTHLY'
|
||||
? { months: 1 }
|
||||
: { years: 1 }
|
||||
|
||||
return datePivot(new Date(sub.billedLastAt), pivot)
|
||||
return datePivot(new Date(relativeTo), pivot)
|
||||
}
|
||||
|
||||
export function nextNextBilling (sub) {
|
||||
if (!sub || sub.billingType === 'ONCE') return null
|
||||
export function purchasedType (sub) {
|
||||
if (!sub?.billPaidUntil) return 'ONCE'
|
||||
return diffDays(new Date(sub.billedLastAt), new Date(sub.billPaidUntil)) >= 365 ? 'YEARLY' : 'MONTHLY'
|
||||
}
|
||||
|
||||
const pivot = sub.billingType === 'MONTHLY'
|
||||
? { months: 2 }
|
||||
: { years: 2 }
|
||||
export function proratedBillingCost (sub, newBillingType) {
|
||||
if (!sub ||
|
||||
sub.billingType === 'ONCE' ||
|
||||
sub.billingType === newBillingType.toUpperCase()) return null
|
||||
|
||||
return datePivot(new Date(sub.billedLastAt), pivot)
|
||||
return TERRITORY_PERIOD_COST(newBillingType) - TERRITORY_PERIOD_COST(purchasedType(sub))
|
||||
}
|
||||
|
||||
export function nextBillingWithGrace (sub) {
|
||||
const dueDate = nextBilling(sub)
|
||||
if (!sub) return null
|
||||
return datePivot(dueDate, { days: TERRITORY_GRACE_DAYS })
|
||||
return datePivot(new Date(sub.billPaidUntil), { days: TERRITORY_GRACE_DAYS })
|
||||
}
|
||||
|
|
|
@ -33,6 +33,11 @@ export function datePivot (date,
|
|||
)
|
||||
}
|
||||
|
||||
export function diffDays (date1, date2) {
|
||||
const diffTime = Math.abs(date2 - date1)
|
||||
return Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
export const dayMonthYear = when => new Date(when).toISOString().slice(0, 10)
|
||||
export const dayMonthYearToDate = when => {
|
||||
const [year, month, day] = when.split('-')
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Sub" ADD COLUMN "billPaidUntil" TIMESTAMP(3);
|
||||
|
||||
-- we want to denomalize billPaidUntil into a job so that the application
|
||||
-- doesn't have to concern itself too much with territory billing jobs
|
||||
-- and think about future state
|
||||
CREATE OR REPLACE FUNCTION update_territory_billing()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF (TG_OP = 'UPDATE') THEN
|
||||
-- delete the old job
|
||||
DELETE FROM pgboss.job
|
||||
WHERE name = 'territoryBilling'
|
||||
AND data->>'subName' = OLD."name";
|
||||
END IF;
|
||||
|
||||
IF (NEW."billPaidUntil" IS NOT NULL) THEN
|
||||
-- create a new job
|
||||
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
|
||||
VALUES (
|
||||
'territoryBilling',
|
||||
jsonb_build_object('subName', NEW.name),
|
||||
NEW."billPaidUntil",
|
||||
NEW."billPaidUntil" + interval '1 day');
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
EXCEPTION WHEN undefined_table THEN
|
||||
return NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS update_territory_billing_trigger ON "Sub";
|
||||
CREATE TRIGGER update_territory_billing_trigger
|
||||
AFTER INSERT OR UPDATE ON "Sub"
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.status = 'ACTIVE')
|
||||
EXECUTE PROCEDURE update_territory_billing();
|
||||
|
||||
-- migrate existing data to have billPaidUntil
|
||||
UPDATE "Sub"
|
||||
SET "billPaidUntil" =
|
||||
(CASE
|
||||
WHEN "billingType" = 'MONTHLY' THEN "billedLastAt" + interval '1 month'
|
||||
WHEN "billingType" = 'YEARLY' THEN "billedLastAt" + interval '1 year'
|
||||
ELSE NULL
|
||||
END);
|
||||
|
|
@ -476,6 +476,7 @@ model Sub {
|
|||
billingCost Int
|
||||
billingAutoRenew Boolean @default(false)
|
||||
billedLastAt DateTime @default(now())
|
||||
billPaidUntil DateTime?
|
||||
moderated Boolean @default(false)
|
||||
moderatedCount Int @default(0)
|
||||
nsfw Boolean @default(false)
|
||||
|
|
Loading…
Reference in New Issue