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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -23,6 +24,7 @@ export default gql`
|
|||
postTypes: [String!]!
|
||||
billingCost: Int!
|
||||
billingType: String!
|
||||
billingAutoRenew: Boolean!
|
||||
rankingType: String!
|
||||
billedLastAt: Date!
|
||||
baseCost: Int!
|
||||
|
|
|
@ -15,9 +15,11 @@ export default function TerritoryForm ({ sub }) {
|
|||
const [upsertSub] = useMutation(
|
||||
gql`
|
||||
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,
|
||||
postTypes: $postTypes, billingType: $billingType, hash: $hash, hmac: $hmac) {
|
||||
postTypes: $postTypes, billingType: $billingType,
|
||||
billingAutoRenew: $billingAutoRenew, hash: $hash, hmac: $hmac) {
|
||||
name
|
||||
}
|
||||
}`
|
||||
|
@ -59,7 +61,8 @@ export default function TerritoryForm ({ sub }) {
|
|||
desc: sub?.desc || '',
|
||||
baseCost: sub?.baseCost || 10,
|
||||
postTypes: sub?.postTypes || POST_TYPES,
|
||||
billingType: sub?.billingType || 'MONTHLY'
|
||||
billingType: sub?.billingType || 'MONTHLY',
|
||||
billingAutoRenew: sub?.billingAutoRenew || false
|
||||
}}
|
||||
schema={territorySchema({ client, me })}
|
||||
invoiceable
|
||||
|
@ -138,7 +141,11 @@ export default function TerritoryForm ({ sub }) {
|
|||
</Col>
|
||||
</Row>
|
||||
</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') &&
|
||||
<Checkbox
|
||||
type='radio'
|
||||
|
@ -170,6 +177,12 @@ export default function TerritoryForm ({ sub }) {
|
|||
groupClassName='ms-1 mb-0'
|
||||
/>}
|
||||
</CheckboxGroup>
|
||||
{billing !== 'once' &&
|
||||
<Checkbox
|
||||
label='auto renew'
|
||||
name='billingAutoRenew'
|
||||
groupClassName='ms-1 mt-2'
|
||||
/>}
|
||||
<div className='mt-3 d-flex justify-content-end'>
|
||||
<FeeButton
|
||||
text={sub ? 'save' : 'found it'}
|
||||
|
|
|
@ -3,23 +3,29 @@ import { useMe } from './me'
|
|||
import FeeButton, { FeeButtonProvider } from './fee-button'
|
||||
import { TERRITORY_BILLING_OPTIONS, TERRITORY_GRACE_DAYS } from '../lib/constants'
|
||||
import { Form } from './form'
|
||||
import { datePivot } from '../lib/time'
|
||||
import { datePivot, timeSince } from '../lib/time'
|
||||
import { LongCountdown } from './countdown'
|
||||
import { useCallback } from 'react'
|
||||
import { useApolloClient, useMutation } from '@apollo/client'
|
||||
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 }) {
|
||||
const me = useMe()
|
||||
const client = useApolloClient()
|
||||
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(
|
||||
async ({ ...variables }) => {
|
||||
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
|
||||
|
||||
const dueDate = billingDueDate(sub, true)
|
||||
|
||||
if (!dueDate) return null
|
||||
|
||||
return (
|
||||
<Alert key='danger' variant='danger'>
|
||||
{sub.status === 'STOPPED'
|
||||
|
@ -75,3 +85,17 @@ export default function TerritoryPaymentDue ({ sub }) {
|
|||
</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
|
||||
billingType
|
||||
billingCost
|
||||
billingAutoRenew
|
||||
billedLastAt
|
||||
baseCost
|
||||
userId
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export function timeSince (timeStamp) {
|
||||
const now = new Date()
|
||||
const secondsPast = (now.getTime() - timeStamp) / 1000
|
||||
const secondsPast = Math.abs(now.getTime() - timeStamp) / 1000
|
||||
if (secondsPast < 60) {
|
||||
return parseInt(secondsPast) + 's'
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import PageLoading from '../../components/page-loading'
|
|||
import CardFooter from 'react-bootstrap/CardFooter'
|
||||
import Hat from '../../components/hat'
|
||||
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 { numWithUnits } from '../../lib/format'
|
||||
|
||||
|
@ -49,17 +49,16 @@ export default function Sub ({ ssrData }) {
|
|||
</div>
|
||||
<CardFooter className={`py-1 ${styles.other}`}>
|
||||
<div className='text-muted'>
|
||||
<span>founded by</span>
|
||||
<span> </span>
|
||||
<span>founded by </span>
|
||||
<Link href={`/${sub.user.name}`}>
|
||||
@{sub.user.name}<span> </span><Hat className='fill-grey' user={sub.user} height={12} width={12} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className='text-muted'>
|
||||
<span>post cost</span>
|
||||
<span> </span>
|
||||
<span>post cost </span>
|
||||
<span className='fw-bold'>{numWithUnits(sub.baseCost)}</span>
|
||||
</div>
|
||||
<TerritoryBillingLine sub={sub} />
|
||||
</CardFooter>
|
||||
</AccordianCard>
|
||||
</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
|
||||
path Unsupported("ltree")?
|
||||
|
||||
postTypes PostType[]
|
||||
rankingType RankingType
|
||||
baseCost Int @default(1)
|
||||
rewardsPct Int @default(50)
|
||||
desc String?
|
||||
status Status @default(ACTIVE)
|
||||
billingType BillingType
|
||||
billingCost Int
|
||||
billedLastAt DateTime @default(now())
|
||||
postTypes PostType[]
|
||||
rankingType RankingType
|
||||
baseCost Int @default(1)
|
||||
rewardsPct Int @default(50)
|
||||
desc String?
|
||||
status Status @default(ACTIVE)
|
||||
billingType BillingType
|
||||
billingCost Int
|
||||
billingAutoRenew Boolean @default(false)
|
||||
billedLastAt DateTime @default(now())
|
||||
|
||||
parent Sub? @relation("ParentChildren", fields: [parentName], references: [name])
|
||||
children Sub[] @relation("ParentChildren")
|
||||
|
|
|
@ -10,12 +10,7 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
|
|||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const queries = paySubQueries(sub, models)
|
||||
await serialize(models, ...queries)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
async function territoryStatusUpdate () {
|
||||
await models.sub.update({
|
||||
where: {
|
||||
name: subName
|
||||
|
@ -27,4 +22,17 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
|
|||
// retry billing in one day
|
||||
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