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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user