territory billing notifications
This commit is contained in:
parent
562b243111
commit
717f8d1ef6
@ -4,6 +4,7 @@ import { getItem, filterClause, whereClause, muteClause } from './item'
|
|||||||
import { getInvoice } from './wallet'
|
import { getInvoice } from './wallet'
|
||||||
import { pushSubscriptionSchema, ssValidate } from '../../lib/validate'
|
import { pushSubscriptionSchema, ssValidate } from '../../lib/validate'
|
||||||
import { replyToSubscription } from '../webPush'
|
import { replyToSubscription } from '../webPush'
|
||||||
|
import { getSub } from './sub'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
@ -249,6 +250,17 @@ export default {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queries.push(
|
||||||
|
`(SELECT "Sub".name::text, "Sub"."statusUpdatedAt" AS "sortTime", NULL as "earnedSats",
|
||||||
|
'SubStatus' AS type
|
||||||
|
FROM "Sub"
|
||||||
|
WHERE "Sub"."userId" = $1
|
||||||
|
AND "status" <> 'ACTIVE'
|
||||||
|
AND "statusUpdatedAt" <= $2
|
||||||
|
ORDER BY "sortTime" DESC
|
||||||
|
LIMIT ${LIMIT}+$3)`
|
||||||
|
)
|
||||||
|
|
||||||
// we do all this crazy subquery stuff to make 'reward' islands
|
// we do all this crazy subquery stuff to make 'reward' islands
|
||||||
const notifications = await models.$queryRawUnsafe(
|
const notifications = await models.$queryRawUnsafe(
|
||||||
`SELECT MAX(id) AS id, MAX("sortTime") AS "sortTime", sum("earnedSats") AS "earnedSats", type,
|
`SELECT MAX(id) AS id, MAX("sortTime") AS "sortTime", sum("earnedSats") AS "earnedSats", type,
|
||||||
@ -339,6 +351,9 @@ export default {
|
|||||||
JobChanged: {
|
JobChanged: {
|
||||||
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
|
item: async (n, args, { models, me }) => getItem(n, { id: n.id }, { models, me })
|
||||||
},
|
},
|
||||||
|
SubStatus: {
|
||||||
|
sub: async (n, args, { models, me }) => getSub(n, { name: n.id }, { models, me })
|
||||||
|
},
|
||||||
Revenue: {
|
Revenue: {
|
||||||
subName: async (n, args, { models }) => {
|
subName: async (n, args, { models }) => {
|
||||||
const subAct = await models.subAct.findUnique({
|
const subAct = await models.subAct.findUnique({
|
||||||
|
@ -3,17 +3,15 @@ import serialize, { 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 } from '../../lib/constants'
|
||||||
import { datePivot } from '../../lib/time'
|
import { datePivot } from '../../lib/time'
|
||||||
import { ssValidate, territorySchema } from '../../lib/validate'
|
import { ssValidate, territorySchema } from '../../lib/validate'
|
||||||
|
import { nextBilling, nextNextBilling } from '../../lib/territory'
|
||||||
|
|
||||||
export function paySubQueries (sub, models) {
|
export function paySubQueries (sub, models) {
|
||||||
let billingAt = datePivot(sub.billedLastAt, { months: 1 })
|
|
||||||
let billAt = datePivot(sub.billedLastAt, { months: 2 })
|
|
||||||
if (sub.billingType === 'ONCE') {
|
if (sub.billingType === 'ONCE') {
|
||||||
return []
|
return []
|
||||||
} else if (sub.billingType === 'YEARLY') {
|
|
||||||
billingAt = datePivot(sub.billedLastAt, { years: 1 })
|
|
||||||
billAt = datePivot(sub.billedLastAt, { years: 2 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const billingAt = nextBilling(sub)
|
||||||
|
const billAt = nextNextBilling(sub)
|
||||||
const cost = BigInt(sub.billingCost) * BigInt(1000)
|
const cost = BigInt(sub.billingCost) * BigInt(1000)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -53,35 +51,37 @@ export function paySubQueries (sub, models) {
|
|||||||
AND completedon IS NULL`,
|
AND completedon IS NULL`,
|
||||||
// schedule 'em
|
// schedule 'em
|
||||||
models.$queryRaw`
|
models.$queryRaw`
|
||||||
INSERT INTO pgboss.job (name, data, startafter) VALUES ('territoryBilling',
|
INSERT INTO pgboss.job (name, data, startafter, keepuntil) VALUES ('territoryBilling',
|
||||||
${JSON.stringify({
|
${JSON.stringify({
|
||||||
subName: sub.name
|
subName: sub.name
|
||||||
})}::JSONB, ${billAt})`
|
})}::JSONB, ${billAt}, ${datePivot(billAt, { days: 1 })})`
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSub (parent, { name }, { models, me }) {
|
||||||
|
if (!name) return null
|
||||||
|
|
||||||
|
return await models.sub.findUnique({
|
||||||
|
where: {
|
||||||
|
name
|
||||||
|
},
|
||||||
|
...(me
|
||||||
|
? {
|
||||||
|
include: {
|
||||||
|
MuteSub: {
|
||||||
|
where: {
|
||||||
|
userId: Number(me?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
sub: async (parent, { name }, { models, me }) => {
|
sub: getSub,
|
||||||
if (!name) return null
|
|
||||||
|
|
||||||
return await models.sub.findUnique({
|
|
||||||
where: {
|
|
||||||
name
|
|
||||||
},
|
|
||||||
...(me
|
|
||||||
? {
|
|
||||||
include: {
|
|
||||||
MuteSub: {
|
|
||||||
where: {
|
|
||||||
userId: Number(me?.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
subs: async (parent, args, { models, me }) => {
|
subs: async (parent, args, { models, me }) => {
|
||||||
if (me) {
|
if (me) {
|
||||||
return await models.$queryRaw`
|
return await models.$queryRaw`
|
||||||
@ -249,10 +249,10 @@ async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
|
|||||||
// schedule 'em
|
// schedule 'em
|
||||||
...(billAt
|
...(billAt
|
||||||
? [models.$queryRaw`
|
? [models.$queryRaw`
|
||||||
INSERT INTO pgboss.job (name, data, startafter) VALUES ('territoryBilling',
|
INSERT INTO pgboss.job (name, data, startafter, keepuntil) VALUES ('territoryBilling',
|
||||||
${JSON.stringify({
|
${JSON.stringify({
|
||||||
subName: data.name
|
subName: data.name
|
||||||
})}::JSONB, ${billAt})`]
|
})}::JSONB, ${billAt}, ${datePivot(billAt, { days: 1 })})`]
|
||||||
: [])
|
: [])
|
||||||
], { models, lnd, hash, hmac, me, enforceFee: billingCost })
|
], { models, lnd, hash, hmac, me, enforceFee: billingCost })
|
||||||
|
|
||||||
|
@ -462,6 +462,23 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subStatus = await models.sub.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: me.id,
|
||||||
|
statusUpdatedAt: {
|
||||||
|
gt: lastChecked
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
not: 'ACTIVE'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (subStatus) {
|
||||||
|
foundNotes()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// update checkedNotesAt to prevent rechecking same time period
|
// update checkedNotesAt to prevent rechecking same time period
|
||||||
models.user.update({
|
models.user.update({
|
||||||
where: { id: me.id },
|
where: { id: me.id },
|
||||||
|
@ -96,9 +96,15 @@ export default gql`
|
|||||||
sortTime: Date!
|
sortTime: Date!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubStatus {
|
||||||
|
id: ID!
|
||||||
|
sub: Sub!
|
||||||
|
sortTime: Date!
|
||||||
|
}
|
||||||
|
|
||||||
union Notification = Reply | Votification | Mention
|
union Notification = Reply | Votification | Mention
|
||||||
| Invitification | Earn | JobChanged | InvoicePaid | Referral
|
| Invitification | Earn | JobChanged | InvoicePaid | Referral
|
||||||
| Streak | FollowActivity | ForwardedVotification | Revenue
|
| Streak | FollowActivity | ForwardedVotification | Revenue | SubStatus
|
||||||
|
|
||||||
type Notifications {
|
type Notifications {
|
||||||
lastChecked: Date
|
lastChecked: Date
|
||||||
|
@ -26,6 +26,8 @@ import Text from './text'
|
|||||||
import NostrIcon from '../svgs/nostr.svg'
|
import NostrIcon from '../svgs/nostr.svg'
|
||||||
import { numWithUnits } from '../lib/format'
|
import { numWithUnits } from '../lib/format'
|
||||||
import BountyIcon from '../svgs/bounty-bag.svg'
|
import BountyIcon from '../svgs/bounty-bag.svg'
|
||||||
|
import { LongCountdown } from './countdown'
|
||||||
|
import { nextBillingWithGrace } from '../lib/territory'
|
||||||
|
|
||||||
function Notification ({ n, fresh }) {
|
function Notification ({ n, fresh }) {
|
||||||
const type = n.__typename
|
const type = n.__typename
|
||||||
@ -44,6 +46,7 @@ function Notification ({ n, fresh }) {
|
|||||||
(type === 'Mention' && <Mention n={n} />) ||
|
(type === 'Mention' && <Mention n={n} />) ||
|
||||||
(type === 'JobChanged' && <JobChanged n={n} />) ||
|
(type === 'JobChanged' && <JobChanged n={n} />) ||
|
||||||
(type === 'Reply' && <Reply n={n} />) ||
|
(type === 'Reply' && <Reply n={n} />) ||
|
||||||
|
(type === 'SubStatus' && <SubStatus n={n} />) ||
|
||||||
(type === 'FollowActivity' && <FollowActivity n={n} />)
|
(type === 'FollowActivity' && <FollowActivity n={n} />)
|
||||||
}
|
}
|
||||||
</NotificationLayout>
|
</NotificationLayout>
|
||||||
@ -86,6 +89,7 @@ const defaultOnClick = n => {
|
|||||||
return { href }
|
return { href }
|
||||||
}
|
}
|
||||||
if (type === 'Revenue') return { href: `/~${n.subName}` }
|
if (type === 'Revenue') return { href: `/~${n.subName}` }
|
||||||
|
if (type === 'SubStatus') return { href: `/~${n.sub.name}` }
|
||||||
if (type === 'Invitification') return { href: '/invites' }
|
if (type === 'Invitification') return { href: '/invites' }
|
||||||
if (type === 'InvoicePaid') return { href: `/invoices/${n.invoice.id}` }
|
if (type === 'InvoicePaid') return { href: `/invoices/${n.invoice.id}` }
|
||||||
if (type === 'Referral') return { href: '/referrals/month' }
|
if (type === 'Referral') return { href: '/referrals/month' }
|
||||||
@ -190,6 +194,20 @@ function RevenueNotification ({ n }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SubStatus ({ n }) {
|
||||||
|
const dueDate = nextBillingWithGrace(n.sub)
|
||||||
|
return (
|
||||||
|
<div className={`fw-bold text-${n.sub.status === 'ACTIVE' ? 'success' : 'danger'} ms-2`}>
|
||||||
|
{n.sub.status === 'ACTIVE'
|
||||||
|
? 'your territory is active again'
|
||||||
|
: (n.sub.status === 'GRACE'
|
||||||
|
? <>your territory payment for ~{n.sub.name} is due or your territory will be archived in <LongCountdown date={dueDate} /></>
|
||||||
|
: <>your territory ~{n.sub.name} has been archived</>)}
|
||||||
|
<small className='text-muted d-block pb-1 fw-normal'>click to visit territory and pay</small>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Invitification ({ n }) {
|
function Invitification ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,25 +1,14 @@
|
|||||||
import { Alert } from 'react-bootstrap'
|
import { Alert } from 'react-bootstrap'
|
||||||
import { useMe } from './me'
|
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 } from '../lib/constants'
|
||||||
import { Form } from './form'
|
import { Form } from './form'
|
||||||
import { datePivot, timeSince } from '../lib/time'
|
import { 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'
|
||||||
|
import { nextBilling, nextBillingWithGrace } from '../lib/territory'
|
||||||
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()
|
||||||
@ -39,8 +28,7 @@ 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)
|
const dueDate = nextBillingWithGrace(sub)
|
||||||
|
|
||||||
if (!dueDate) return null
|
if (!dueDate) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -90,12 +78,13 @@ export function TerritoryBillingLine ({ sub }) {
|
|||||||
const me = useMe()
|
const me = useMe()
|
||||||
if (!sub || sub.userId !== Number(me?.id)) return null
|
if (!sub || sub.userId !== Number(me?.id)) return null
|
||||||
|
|
||||||
const dueDate = billingDueDate(sub, false)
|
const dueDate = nextBilling(sub)
|
||||||
|
const pastDue = dueDate && dueDate < new Date()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='text-muted'>
|
<div className='text-muted'>
|
||||||
<span>billing {sub.billingAutoRenew ? 'automatically renews' : 'due'} on </span>
|
<span>billing {sub.billingAutoRenew ? 'automatically renews' : 'due'} </span>
|
||||||
<span className='fw-bold'>{dueDate ? timeSince(dueDate) : 'never again'}</span>
|
<span className='fw-bold'>{pastDue ? 'past due' : dueDate ? timeSince(dueDate) : 'never again'}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { gql } from '@apollo/client'
|
import { gql } from '@apollo/client'
|
||||||
import { ITEM_FULL_FIELDS } from './items'
|
import { ITEM_FULL_FIELDS } from './items'
|
||||||
import { INVITE_FIELDS } from './invites'
|
import { INVITE_FIELDS } from './invites'
|
||||||
|
import { SUB_FIELDS } from './subs'
|
||||||
|
|
||||||
export const HAS_NOTIFICATIONS = gql`{ hasNewNotes }`
|
export const HAS_NOTIFICATIONS = gql`{ hasNewNotes }`
|
||||||
|
|
||||||
export const NOTIFICATIONS = gql`
|
export const NOTIFICATIONS = gql`
|
||||||
${ITEM_FULL_FIELDS}
|
${ITEM_FULL_FIELDS}
|
||||||
${INVITE_FIELDS}
|
${INVITE_FIELDS}
|
||||||
|
${SUB_FIELDS}
|
||||||
|
|
||||||
query Notifications($cursor: String, $inc: String) {
|
query Notifications($cursor: String, $inc: String) {
|
||||||
notifications(cursor: $cursor, inc: $inc) {
|
notifications(cursor: $cursor, inc: $inc) {
|
||||||
@ -98,6 +100,13 @@ export const NOTIFICATIONS = gql`
|
|||||||
...ItemFields
|
...ItemFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
... on SubStatus {
|
||||||
|
id
|
||||||
|
sortTime
|
||||||
|
sub {
|
||||||
|
...SubFields
|
||||||
|
}
|
||||||
|
}
|
||||||
... on InvoicePaid {
|
... on InvoicePaid {
|
||||||
id
|
id
|
||||||
sortTime
|
sortTime
|
||||||
|
28
lib/territory.js
Normal file
28
lib/territory.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { TERRITORY_GRACE_DAYS } from './constants'
|
||||||
|
import { datePivot } from './time'
|
||||||
|
|
||||||
|
export function nextBilling (sub) {
|
||||||
|
if (!sub || sub.billingType === 'ONCE') return null
|
||||||
|
|
||||||
|
const pivot = sub.billingType === 'MONTHLY'
|
||||||
|
? { months: 1 }
|
||||||
|
: { years: 1 }
|
||||||
|
|
||||||
|
return datePivot(new Date(sub.billedLastAt), pivot)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextNextBilling (sub) {
|
||||||
|
if (!sub || sub.billingType === 'ONCE') return null
|
||||||
|
|
||||||
|
const pivot = sub.billingType === 'MONTHLY'
|
||||||
|
? { months: 2 }
|
||||||
|
: { years: 2 }
|
||||||
|
|
||||||
|
return datePivot(new Date(sub.billedLastAt), pivot)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextBillingWithGrace (sub) {
|
||||||
|
const dueDate = nextBilling(sub)
|
||||||
|
if (!sub) return null
|
||||||
|
return datePivot(dueDate, { days: TERRITORY_GRACE_DAYS })
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Sub" ADD COLUMN "statusUpdatedAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Sub_statusUpdatedAt_idx" ON "Sub"("statusUpdatedAt");
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION reset_territory_billing_job()
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM pgboss.job where name = 'territoryBilling';
|
||||||
|
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
|
||||||
|
SELECT 'territoryBilling', json_build_object('subName', name),
|
||||||
|
"billedLastAt" + CASE WHEN "billingType" = 'MONTHLY' THEN interval '1 month' ELSE interval '1 year' END,
|
||||||
|
"billedLastAt" + CASE WHEN "billingType" = 'MONTHLY' THEN interval '1 month 1 day' ELSE interval '1 year 1 day' END
|
||||||
|
FROM "Sub"
|
||||||
|
WHERE "billingType" <> 'ONCE';
|
||||||
|
return 0;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
return 0;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
SELECT reset_territory_billing_job();
|
||||||
|
DROP FUNCTION reset_territory_billing_job();
|
@ -412,6 +412,7 @@ model Sub {
|
|||||||
rewardsPct Int @default(50)
|
rewardsPct Int @default(50)
|
||||||
desc String?
|
desc String?
|
||||||
status Status @default(ACTIVE)
|
status Status @default(ACTIVE)
|
||||||
|
statusUpdatedAt DateTime?
|
||||||
billingType BillingType
|
billingType BillingType
|
||||||
billingCost Int
|
billingCost Int
|
||||||
billingAutoRenew Boolean @default(false)
|
billingAutoRenew Boolean @default(false)
|
||||||
@ -429,6 +430,7 @@ model Sub {
|
|||||||
@@index([parentName])
|
@@index([parentName])
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
@@index([statusUpdatedAt])
|
||||||
@@index([path], type: Gist)
|
@@index([path], type: Gist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import serialize from '../api/resolvers/serial'
|
import serialize from '../api/resolvers/serial'
|
||||||
import { paySubQueries } from '../api/resolvers/sub'
|
import { paySubQueries } from '../api/resolvers/sub'
|
||||||
import { TERRITORY_GRACE_DAYS } from '../lib/constants'
|
import { nextBillingWithGrace } from '../lib/territory'
|
||||||
import { datePivot } from '../lib/time'
|
import { datePivot } from '../lib/time'
|
||||||
|
|
||||||
export async function territoryBilling ({ data: { subName }, boss, models }) {
|
export async function territoryBilling ({ data: { subName }, boss, models }) {
|
||||||
@ -11,14 +11,18 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function territoryStatusUpdate () {
|
async function territoryStatusUpdate () {
|
||||||
await models.sub.update({
|
if (sub.status !== 'STOPPED') {
|
||||||
where: {
|
await models.sub.update({
|
||||||
name: subName
|
where: {
|
||||||
},
|
name: subName
|
||||||
data: {
|
},
|
||||||
status: sub.billedLastAt >= datePivot(new Date(), { days: -TERRITORY_GRACE_DAYS }) ? 'GRACE' : 'STOPPED'
|
data: {
|
||||||
}
|
status: nextBillingWithGrace(sub) >= new Date() ? 'GRACE' : 'STOPPED',
|
||||||
})
|
statusUpdatedAt: new Date()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 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 }) })
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user