make territory billing renewal opt-in
This commit is contained in:
parent
89ecb8c0b9
commit
1d66be68cc
|
@ -9,7 +9,8 @@ export default gql`
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
upsertSub(name: String!, desc: String, baseCost: Int!,
|
upsertSub(name: String!, desc: String, baseCost: Int!,
|
||||||
postTypes: [String!]!, billingType: String!, hash: String, hmac: String): Sub
|
postTypes: [String!]!, billingType: String!, billingAutoRenew: Boolean!,
|
||||||
|
hash: String, hmac: String): Sub
|
||||||
paySub(name: String!, hash: String, hmac: String): Sub
|
paySub(name: String!, hash: String, hmac: String): Sub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ export default gql`
|
||||||
postTypes: [String!]!
|
postTypes: [String!]!
|
||||||
billingCost: Int!
|
billingCost: Int!
|
||||||
billingType: String!
|
billingType: String!
|
||||||
|
billingAutoRenew: Boolean!
|
||||||
rankingType: String!
|
rankingType: String!
|
||||||
billedLastAt: Date!
|
billedLastAt: Date!
|
||||||
baseCost: Int!
|
baseCost: Int!
|
||||||
|
|
|
@ -15,9 +15,11 @@ export default function TerritoryForm ({ sub }) {
|
||||||
const [upsertSub] = useMutation(
|
const [upsertSub] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation upsertSub($name: String!, $desc: String, $baseCost: Int!,
|
mutation upsertSub($name: String!, $desc: String, $baseCost: Int!,
|
||||||
$postTypes: [String!]!, $billingType: String!, $hash: String, $hmac: String) {
|
$postTypes: [String!]!, $billingType: String!, $billingAutoRenew: Boolean!,
|
||||||
|
$hash: String, $hmac: String) {
|
||||||
upsertSub(name: $name, desc: $desc, baseCost: $baseCost,
|
upsertSub(name: $name, desc: $desc, baseCost: $baseCost,
|
||||||
postTypes: $postTypes, billingType: $billingType, hash: $hash, hmac: $hmac) {
|
postTypes: $postTypes, billingType: $billingType,
|
||||||
|
billingAutoRenew: $billingAutoRenew, hash: $hash, hmac: $hmac) {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
@ -59,7 +61,8 @@ export default function TerritoryForm ({ sub }) {
|
||||||
desc: sub?.desc || '',
|
desc: sub?.desc || '',
|
||||||
baseCost: sub?.baseCost || 10,
|
baseCost: sub?.baseCost || 10,
|
||||||
postTypes: sub?.postTypes || POST_TYPES,
|
postTypes: sub?.postTypes || POST_TYPES,
|
||||||
billingType: sub?.billingType || 'MONTHLY'
|
billingType: sub?.billingType || 'MONTHLY',
|
||||||
|
billingAutoRenew: sub?.billingAutoRenew || false
|
||||||
}}
|
}}
|
||||||
schema={territorySchema({ client, me })}
|
schema={territorySchema({ client, me })}
|
||||||
invoiceable
|
invoiceable
|
||||||
|
@ -138,7 +141,11 @@ export default function TerritoryForm ({ sub }) {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</CheckboxGroup>
|
</CheckboxGroup>
|
||||||
<CheckboxGroup label={sub ? <>name <small className='text-muted ms-2'>read only</small></> : 'billing'} name='billing'>
|
<CheckboxGroup
|
||||||
|
label='billing'
|
||||||
|
name='billing'
|
||||||
|
groupClassName='mb-0'
|
||||||
|
>
|
||||||
{(!sub?.billingType || sub.billingType === 'MONTHLY') &&
|
{(!sub?.billingType || sub.billingType === 'MONTHLY') &&
|
||||||
<Checkbox
|
<Checkbox
|
||||||
type='radio'
|
type='radio'
|
||||||
|
@ -170,6 +177,12 @@ export default function TerritoryForm ({ sub }) {
|
||||||
groupClassName='ms-1 mb-0'
|
groupClassName='ms-1 mb-0'
|
||||||
/>}
|
/>}
|
||||||
</CheckboxGroup>
|
</CheckboxGroup>
|
||||||
|
{billing !== 'once' &&
|
||||||
|
<Checkbox
|
||||||
|
label='auto renew'
|
||||||
|
name='billingAutoRenew'
|
||||||
|
groupClassName='ms-1 mt-2'
|
||||||
|
/>}
|
||||||
<div className='mt-3 d-flex justify-content-end'>
|
<div className='mt-3 d-flex justify-content-end'>
|
||||||
<FeeButton
|
<FeeButton
|
||||||
text={sub ? 'save' : 'found it'}
|
text={sub ? 'save' : 'found it'}
|
||||||
|
|
|
@ -3,23 +3,29 @@ import { useMe } from './me'
|
||||||
import FeeButton, { FeeButtonProvider } from './fee-button'
|
import FeeButton, { FeeButtonProvider } from './fee-button'
|
||||||
import { TERRITORY_BILLING_OPTIONS, TERRITORY_GRACE_DAYS } from '../lib/constants'
|
import { TERRITORY_BILLING_OPTIONS, TERRITORY_GRACE_DAYS } from '../lib/constants'
|
||||||
import { Form } from './form'
|
import { Form } from './form'
|
||||||
import { datePivot } from '../lib/time'
|
import { datePivot, timeSince } from '../lib/time'
|
||||||
import { LongCountdown } from './countdown'
|
import { LongCountdown } from './countdown'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useApolloClient, useMutation } from '@apollo/client'
|
import { useApolloClient, useMutation } from '@apollo/client'
|
||||||
import { SUB_PAY } from '../fragments/subs'
|
import { SUB_PAY } from '../fragments/subs'
|
||||||
|
|
||||||
|
const billingDueDate = (sub, grace) => {
|
||||||
|
if (!sub || sub.billingType === 'ONCE') return null
|
||||||
|
|
||||||
|
const pivot = sub.billingType === 'MONTHLY'
|
||||||
|
? { months: 1 }
|
||||||
|
: { years: 1 }
|
||||||
|
|
||||||
|
pivot.days = grace ? TERRITORY_GRACE_DAYS : 0
|
||||||
|
|
||||||
|
return datePivot(new Date(sub.billedLastAt), pivot)
|
||||||
|
}
|
||||||
|
|
||||||
export default function TerritoryPaymentDue ({ sub }) {
|
export default function TerritoryPaymentDue ({ sub }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
const [paySub] = useMutation(SUB_PAY)
|
const [paySub] = useMutation(SUB_PAY)
|
||||||
|
|
||||||
const dueDate = datePivot(
|
|
||||||
new Date(sub.billedLastAt),
|
|
||||||
sub.billingType === 'MONTHLY'
|
|
||||||
? { months: 1, days: TERRITORY_GRACE_DAYS }
|
|
||||||
: { years: 1, days: TERRITORY_GRACE_DAYS })
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async ({ ...variables }) => {
|
async ({ ...variables }) => {
|
||||||
const { error } = await paySub({
|
const { error } = await paySub({
|
||||||
|
@ -33,6 +39,10 @@ export default function TerritoryPaymentDue ({ sub }) {
|
||||||
|
|
||||||
if (!sub || sub.userId !== Number(me?.id) || sub.status === 'ACTIVE') return null
|
if (!sub || sub.userId !== Number(me?.id) || sub.status === 'ACTIVE') return null
|
||||||
|
|
||||||
|
const dueDate = billingDueDate(sub, true)
|
||||||
|
|
||||||
|
if (!dueDate) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert key='danger' variant='danger'>
|
<Alert key='danger' variant='danger'>
|
||||||
{sub.status === 'STOPPED'
|
{sub.status === 'STOPPED'
|
||||||
|
@ -75,3 +85,17 @@ export default function TerritoryPaymentDue ({ sub }) {
|
||||||
</Alert>
|
</Alert>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function TerritoryBillingLine ({ sub }) {
|
||||||
|
const me = useMe()
|
||||||
|
if (!sub || sub.userId !== Number(me?.id)) return null
|
||||||
|
|
||||||
|
const dueDate = billingDueDate(sub, false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='text-muted'>
|
||||||
|
<span>billing {sub.billingAutoRenew ? 'automatically renews' : 'due'} on </span>
|
||||||
|
<span className='fw-bold'>{dueDate ? timeSince(dueDate) : 'never again'}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ export const SUB_FIELDS = gql`
|
||||||
rankingType
|
rankingType
|
||||||
billingType
|
billingType
|
||||||
billingCost
|
billingCost
|
||||||
|
billingAutoRenew
|
||||||
billedLastAt
|
billedLastAt
|
||||||
baseCost
|
baseCost
|
||||||
userId
|
userId
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export function timeSince (timeStamp) {
|
export function timeSince (timeStamp) {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const secondsPast = (now.getTime() - timeStamp) / 1000
|
const secondsPast = Math.abs(now.getTime() - timeStamp) / 1000
|
||||||
if (secondsPast < 60) {
|
if (secondsPast < 60) {
|
||||||
return parseInt(secondsPast) + 's'
|
return parseInt(secondsPast) + 's'
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import PageLoading from '../../components/page-loading'
|
||||||
import CardFooter from 'react-bootstrap/CardFooter'
|
import CardFooter from 'react-bootstrap/CardFooter'
|
||||||
import Hat from '../../components/hat'
|
import Hat from '../../components/hat'
|
||||||
import styles from '../../components/item.module.css'
|
import styles from '../../components/item.module.css'
|
||||||
import TerritoryPaymentDue from '../../components/territory-payment-due'
|
import TerritoryPaymentDue, { TerritoryBillingLine } from '../../components/territory-payment-due'
|
||||||
import Badge from 'react-bootstrap/Badge'
|
import Badge from 'react-bootstrap/Badge'
|
||||||
import { numWithUnits } from '../../lib/format'
|
import { numWithUnits } from '../../lib/format'
|
||||||
|
|
||||||
|
@ -49,17 +49,16 @@ export default function Sub ({ ssrData }) {
|
||||||
</div>
|
</div>
|
||||||
<CardFooter className={`py-1 ${styles.other}`}>
|
<CardFooter className={`py-1 ${styles.other}`}>
|
||||||
<div className='text-muted'>
|
<div className='text-muted'>
|
||||||
<span>founded by</span>
|
<span>founded by </span>
|
||||||
<span> </span>
|
|
||||||
<Link href={`/${sub.user.name}`}>
|
<Link href={`/${sub.user.name}`}>
|
||||||
@{sub.user.name}<span> </span><Hat className='fill-grey' user={sub.user} height={12} width={12} />
|
@{sub.user.name}<span> </span><Hat className='fill-grey' user={sub.user} height={12} width={12} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-muted'>
|
<div className='text-muted'>
|
||||||
<span>post cost</span>
|
<span>post cost </span>
|
||||||
<span> </span>
|
|
||||||
<span className='fw-bold'>{numWithUnits(sub.baseCost)}</span>
|
<span className='fw-bold'>{numWithUnits(sub.baseCost)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<TerritoryBillingLine sub={sub} />
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</AccordianCard>
|
</AccordianCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Sub" ADD COLUMN "billingAutoRenew" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -403,15 +403,16 @@ model Sub {
|
||||||
parentName String? @db.Citext
|
parentName String? @db.Citext
|
||||||
path Unsupported("ltree")?
|
path Unsupported("ltree")?
|
||||||
|
|
||||||
postTypes PostType[]
|
postTypes PostType[]
|
||||||
rankingType RankingType
|
rankingType RankingType
|
||||||
baseCost Int @default(1)
|
baseCost Int @default(1)
|
||||||
rewardsPct Int @default(50)
|
rewardsPct Int @default(50)
|
||||||
desc String?
|
desc String?
|
||||||
status Status @default(ACTIVE)
|
status Status @default(ACTIVE)
|
||||||
billingType BillingType
|
billingType BillingType
|
||||||
billingCost Int
|
billingCost Int
|
||||||
billedLastAt DateTime @default(now())
|
billingAutoRenew Boolean @default(false)
|
||||||
|
billedLastAt DateTime @default(now())
|
||||||
|
|
||||||
parent Sub? @relation("ParentChildren", fields: [parentName], references: [name])
|
parent Sub? @relation("ParentChildren", fields: [parentName], references: [name])
|
||||||
children Sub[] @relation("ParentChildren")
|
children Sub[] @relation("ParentChildren")
|
||||||
|
|
|
@ -10,12 +10,7 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
async function territoryStatusUpdate () {
|
||||||
const queries = paySubQueries(sub, models)
|
|
||||||
await serialize(models, ...queries)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
|
|
||||||
await models.sub.update({
|
await models.sub.update({
|
||||||
where: {
|
where: {
|
||||||
name: subName
|
name: subName
|
||||||
|
@ -27,4 +22,17 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
|
||||||
// retry billing in one day
|
// retry billing in one day
|
||||||
await boss.send('territoryBilling', { subName }, { startAfter: datePivot(new Date(), { days: 1 }) })
|
await boss.send('territoryBilling', { subName }, { startAfter: datePivot(new Date(), { days: 1 }) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sub.billingAutoRenew) {
|
||||||
|
await territoryStatusUpdate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const queries = paySubQueries(sub, models)
|
||||||
|
await serialize(models, ...queries)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
await territoryStatusUpdate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue