make jobs great again
This commit is contained in:
		
							parent
							
								
									52fab60cda
								
							
						
					
					
						commit
						46ea2f661c
					
				| @ -201,7 +201,7 @@ export default { | ||||
|                   WHERE "parentId" IS NULL AND created_at <= $1 | ||||
|                   AND "pinId" IS NULL | ||||
|                   ${subClause(3)} | ||||
|                   AND status = 'ACTIVE' | ||||
|                   AND status = 'ACTIVE' AND "maxBid" > 0 | ||||
|                   ORDER BY "maxBid" DESC, created_at ASC) | ||||
|                   UNION ALL | ||||
|                   (${SELECT} | ||||
| @ -209,7 +209,7 @@ export default { | ||||
|                   WHERE "parentId" IS NULL AND created_at <= $1 | ||||
|                   AND "pinId" IS NULL | ||||
|                   ${subClause(3)} | ||||
|                   AND status = 'NOSATS' | ||||
|                   AND ((status = 'ACTIVE' AND "maxBid" = 0) OR status = 'NOSATS') | ||||
|                   ORDER BY created_at DESC) | ||||
|                 ) a | ||||
|                 OFFSET $2 | ||||
| @ -456,11 +456,19 @@ export default { | ||||
|                         bool: { | ||||
|                           should: [ | ||||
|                             { match: { status: 'ACTIVE' } }, | ||||
|                             { match: { status: 'NOSATS' } }, | ||||
|                             { match: { userId: me.id } } | ||||
|                           ] | ||||
|                         } | ||||
|                       } | ||||
|                     : { match: { status: 'ACTIVE' } }, | ||||
|                     : { | ||||
|                         bool: { | ||||
|                           should: [ | ||||
|                             { match: { status: 'ACTIVE' } }, | ||||
|                             { match: { status: 'NOSATS' } } | ||||
|                           ] | ||||
|                         } | ||||
|                       }, | ||||
|                   { | ||||
|                     bool: { | ||||
|                       should: [ | ||||
| @ -544,19 +552,26 @@ export default { | ||||
|         items | ||||
|       } | ||||
|     }, | ||||
|     auctionPosition: async (parent, { id, sub, bid }, { models }) => { | ||||
|     auctionPosition: async (parent, { id, sub, bid }, { models, me }) => { | ||||
|       // count items that have a bid gte to the current bid or
 | ||||
|       // gte current bid and older
 | ||||
|       const where = { | ||||
|         where: { | ||||
|           subName: sub, | ||||
|           status: 'ACTIVE', | ||||
|           maxBid: { | ||||
|             gte: bid | ||||
|           } | ||||
|           status: { not: 'STOPPED' } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (bid > 0) { | ||||
|         where.where.maxBid = { gte: bid } | ||||
|       } else { | ||||
|         const createdAt = id ? (await getItem(parent, { id }, { models, me })).createdAt : new Date() | ||||
|         where.where.OR = [ | ||||
|           { maxBid: { gt: 0 } }, | ||||
|           { createdAt: { gt: createdAt } } | ||||
|         ] | ||||
|       } | ||||
| 
 | ||||
|       if (id) { | ||||
|         where.where.id = { not: Number(id) } | ||||
|       } | ||||
| @ -646,62 +661,36 @@ export default { | ||||
|         throw new UserInputError('not a valid sub', { argumentName: 'sub' }) | ||||
|       } | ||||
| 
 | ||||
|       if (fullSub.baseCost > maxBid) { | ||||
|         throw new UserInputError(`bid must be at least ${fullSub.baseCost}`, { argumentName: 'maxBid' }) | ||||
|       if (maxBid < 0) { | ||||
|         throw new UserInputError('bid must be at least 0', { argumentName: 'maxBid' }) | ||||
|       } | ||||
| 
 | ||||
|       if (!location && !remote) { | ||||
|         throw new UserInputError('must specify location or remote', { argumentName: 'location' }) | ||||
|       } | ||||
| 
 | ||||
|       const checkSats = async () => { | ||||
|         // check if the user has the funds to run for the first minute
 | ||||
|         const minuteMsats = maxBid * 1000 | ||||
|         const user = await models.user.findUnique({ where: { id: me.id } }) | ||||
|         if (user.msats < minuteMsats) { | ||||
|           throw new UserInputError('insufficient funds') | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       const data = { | ||||
|         title, | ||||
|         company, | ||||
|         location: location.toLowerCase() === 'remote' ? undefined : location, | ||||
|         remote, | ||||
|         text, | ||||
|         url, | ||||
|         maxBid, | ||||
|         subName: sub, | ||||
|         userId: me.id, | ||||
|         uploadId: logo | ||||
|       } | ||||
|       location = location.toLowerCase() === 'remote' ? undefined : location | ||||
| 
 | ||||
|       let item | ||||
|       if (id) { | ||||
|         if (status) { | ||||
|           data.status = status | ||||
| 
 | ||||
|           // if the job is changing to active, we need to check they have funds
 | ||||
|           if (status === 'ACTIVE') { | ||||
|             await checkSats() | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         const old = await models.item.findUnique({ where: { id: Number(id) } }) | ||||
|         if (Number(old.userId) !== Number(me?.id)) { | ||||
|           throw new AuthenticationError('item does not belong to you') | ||||
|         } | ||||
| 
 | ||||
|         return await models.item.update({ | ||||
|           where: { id: Number(id) }, | ||||
|           data | ||||
|         }) | ||||
|         ([item] = await serialize(models, | ||||
|           models.$queryRaw( | ||||
|             `${SELECT} FROM update_job($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) AS "Item"`, | ||||
|             Number(id), title, url, text, Number(maxBid), company, location, remote, Number(logo), status))) | ||||
|       } else { | ||||
|         ([item] = await serialize(models, | ||||
|           models.$queryRaw( | ||||
|             `${SELECT} FROM create_job($1, $2, $3, $4, $5, $6, $7, $8, $9) AS "Item"`, | ||||
|             title, url, text, Number(me.id), Number(maxBid), company, location, remote, Number(logo)))) | ||||
|       } | ||||
| 
 | ||||
|       // before creating job, check the sats
 | ||||
|       await checkSats() | ||||
|       return await models.item.create({ | ||||
|         data | ||||
|       }) | ||||
|       await createMentions(item, models) | ||||
| 
 | ||||
|       return item | ||||
|     }, | ||||
|     createComment: async (parent, { text, parentId }, { me, models }) => { | ||||
|       return await createItem(parent, { text, parentId }, { me, models }) | ||||
| @ -767,6 +756,9 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   Item: { | ||||
|     isJob: async (item, args, { models }) => { | ||||
|       return item.subName === 'jobs' | ||||
|     }, | ||||
|     sub: async (item, args, { models }) => { | ||||
|       if (!item.subName) { | ||||
|         return null | ||||
|  | ||||
| @ -98,7 +98,6 @@ export default { | ||||
|             FROM "Item" | ||||
|             WHERE "Item"."userId" = $1 | ||||
|             AND "maxBid" IS NOT NULL | ||||
|             AND status <> 'STOPPED' | ||||
|             AND "statusUpdatedAt" <= $2 | ||||
|             ORDER BY "sortTime" DESC | ||||
|             LIMIT ${LIMIT}+$3)` | ||||
|  | ||||
| @ -337,9 +337,6 @@ export default { | ||||
| 
 | ||||
|       const job = await models.item.findFirst({ | ||||
|         where: { | ||||
|           status: { | ||||
|             not: 'STOPPED' | ||||
|           }, | ||||
|           maxBid: { | ||||
|             not: null | ||||
|           }, | ||||
|  | ||||
| @ -92,6 +92,7 @@ export default gql` | ||||
|     position: Int | ||||
|     prior: Int | ||||
|     maxBid: Int | ||||
|     isJob: Boolean! | ||||
|     pollCost: Int | ||||
|     poll: Poll | ||||
|     company: String | ||||
|  | ||||
| @ -83,7 +83,7 @@ function ItemEmbed ({ item }) { | ||||
| } | ||||
| 
 | ||||
| function TopLevelItem ({ item, noReply, ...props }) { | ||||
|   const ItemComponent = item.maxBid ? ItemJob : Item | ||||
|   const ItemComponent = item.isJob ? ItemJob : Item | ||||
| 
 | ||||
|   return ( | ||||
|     <ItemComponent item={item} toc showFwdUser {...props}> | ||||
|  | ||||
| @ -18,7 +18,7 @@ export default function ItemJob ({ item, toc, rank, children }) { | ||||
|             {rank} | ||||
|           </div>) | ||||
|         : <div />} | ||||
|       <div className={`${styles.item} ${item.status === 'NOSATS' && !item.mine ? styles.itemDead : ''}`}> | ||||
|       <div className={`${styles.item}`}> | ||||
|         <Link href={`/items/${item.id}`} passHref> | ||||
|           <a> | ||||
|             <Image | ||||
| @ -38,11 +38,6 @@ export default function ItemJob ({ item, toc, rank, children }) { | ||||
|             </Link> | ||||
|           </div> | ||||
|           <div className={`${styles.other}`}> | ||||
|             {item.status === 'NOSATS' && | ||||
|               <> | ||||
|                 <span>expired</span> | ||||
|                 {item.company && <span> \ </span>} | ||||
|               </>} | ||||
|             {item.company && | ||||
|               <> | ||||
|                 {item.company} | ||||
| @ -72,7 +67,7 @@ export default function ItemJob ({ item, toc, rank, children }) { | ||||
|                     edit | ||||
|                   </a> | ||||
|                 </Link> | ||||
|                 {item.status !== 'ACTIVE' && <span className='font-weight-bold text-danger'> {item.status}</span>} | ||||
|                 {item.status !== 'ACTIVE' && <span className='ml-1 font-weight-bold text-boost'> {item.status}</span>} | ||||
|               </>} | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
| @ -32,7 +32,7 @@ export default function MixedItems ({ rank, items, cursor, fetchMore }) { | ||||
|                     <Comment item={item} noReply includeParent clickToContext /> | ||||
|                   </div> | ||||
|                 </>) | ||||
|               : (item.maxBid | ||||
|               : (item.isJob | ||||
|                   ? <ItemJob item={item} rank={rank && i + 1} /> | ||||
|                   : <Item item={item} rank={rank && i + 1} />)} | ||||
|           </React.Fragment> | ||||
|  | ||||
| @ -28,7 +28,7 @@ export default function Items ({ variables = {}, rank, items, pins, cursor }) { | ||||
|             {pinMap && pinMap[i + 1] && <Item item={pinMap[i + 1]} />} | ||||
|             {item.parentId | ||||
|               ? <><div /><div className='pb-3'><Comment item={item} noReply includeParent /></div></> | ||||
|               : (item.maxBid | ||||
|               : (item.isJob | ||||
|                   ? <ItemJob item={item} rank={rank && i + 1} /> | ||||
|                   : (item.title | ||||
|                       ? <Item item={item} rank={rank && i + 1} /> | ||||
|  | ||||
| @ -11,6 +11,8 @@ import { useRouter } from 'next/router' | ||||
| import Link from 'next/link' | ||||
| import { usePrice } from './price' | ||||
| import Avatar from './avatar' | ||||
| import BootstrapForm from 'react-bootstrap/Form' | ||||
| import Alert from 'react-bootstrap/Alert' | ||||
| 
 | ||||
| Yup.addMethod(Yup.string, 'or', function (schemas, msg) { | ||||
|   return this.test({ | ||||
| @ -34,7 +36,7 @@ function satsMin2Mo (minute) { | ||||
| 
 | ||||
| function PriceHint ({ monthly }) { | ||||
|   const price = usePrice() | ||||
|   if (!price) { | ||||
|   if (!price || !monthly) { | ||||
|     return null | ||||
|   } | ||||
|   const fixed = (n, f) => Number.parseFloat(n).toFixed(f) | ||||
| @ -47,13 +49,7 @@ function PriceHint ({ monthly }) { | ||||
| export default function JobForm ({ item, sub }) { | ||||
|   const storageKeyPrefix = item ? undefined : `${sub.name}-job` | ||||
|   const router = useRouter() | ||||
|   const [monthly, setMonthly] = useState(satsMin2Mo(item?.maxBid || sub.baseCost)) | ||||
|   const [logoId, setLogoId] = useState(item?.uploadId) | ||||
|   const [getAuctionPosition, { data }] = useLazyQuery(gql` | ||||
|     query AuctionPosition($id: ID, $bid: Int!) { | ||||
|       auctionPosition(sub: "${sub.name}", id: $id, bid: $bid) | ||||
|     }`,
 | ||||
|   { fetchPolicy: 'network-only' }) | ||||
|   const [upsertJob] = useMutation(gql` | ||||
|     mutation upsertJob($id: ID, $title: String!, $company: String!, $location: String, | ||||
|       $remote: Boolean, $text: String!, $url: String!, $maxBid: Int!, $status: String, $logo: Int) { | ||||
| @ -72,8 +68,8 @@ export default function JobForm ({ item, sub }) { | ||||
|     url: Yup.string() | ||||
|       .or([Yup.string().email(), Yup.string().url()], 'invalid url or email') | ||||
|       .required('required'), | ||||
|     maxBid: Yup.number('must be number') | ||||
|       .integer('must be whole').min(sub.baseCost, `must be at least ${sub.baseCost}`) | ||||
|     maxBid: Yup.number().typeError('must be a number') | ||||
|       .integer('must be whole').min(0, 'must be positive') | ||||
|       .required('required'), | ||||
|     location: Yup.string().test( | ||||
|       'no-remote', | ||||
| @ -85,14 +81,6 @@ export default function JobForm ({ item, sub }) { | ||||
|       }) | ||||
|   }) | ||||
| 
 | ||||
|   const position = data?.auctionPosition | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const initialMaxBid = Number(item?.maxBid || localStorage.getItem(storageKeyPrefix + '-maxBid')) || sub.baseCost | ||||
|     getAuctionPosition({ variables: { id: item?.id, bid: initialMaxBid } }) | ||||
|     setMonthly(satsMin2Mo(initialMaxBid)) | ||||
|   }, []) | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Form | ||||
| @ -104,7 +92,7 @@ export default function JobForm ({ item, sub }) { | ||||
|           remote: item?.remote || false, | ||||
|           text: item?.text || '', | ||||
|           url: item?.url || '', | ||||
|           maxBid: item?.maxBid || sub.baseCost, | ||||
|           maxBid: item?.maxBid || 0, | ||||
|           stop: false, | ||||
|           start: false | ||||
|         }} | ||||
| @ -188,32 +176,7 @@ export default function JobForm ({ item, sub }) { | ||||
|           required | ||||
|           clear | ||||
|         /> | ||||
|         <Input | ||||
|           label={ | ||||
|             <div className='d-flex align-items-center'>bid | ||||
|               <Info> | ||||
|                 <ol className='font-weight-bold'> | ||||
|                   <li>The higher your bid the higher your job will rank</li> | ||||
|                   <li>The minimum bid is {sub.baseCost} sats/min</li> | ||||
|                   <li>You can increase or decrease your bid, and edit or stop your job at anytime</li> | ||||
|                   <li>Your job will be hidden if your wallet runs out of sats and can be unhidden by filling your wallet again</li> | ||||
|                 </ol> | ||||
|               </Info> | ||||
|             </div> | ||||
|           } | ||||
|           name='maxBid' | ||||
|           onChange={async (formik, e) => { | ||||
|             if (e.target.value >= sub.baseCost && e.target.value <= 100000000) { | ||||
|               setMonthly(satsMin2Mo(e.target.value)) | ||||
|               getAuctionPosition({ variables: { id: item?.id, bid: Number(e.target.value) } }) | ||||
|             } else { | ||||
|               setMonthly(satsMin2Mo(sub.baseCost)) | ||||
|             } | ||||
|           }} | ||||
|           append={<InputGroup.Text className='text-monospace'>sats/min</InputGroup.Text>} | ||||
|           hint={<PriceHint monthly={monthly} />} | ||||
|         /> | ||||
|         <><div className='font-weight-bold text-muted'>This bid puts your job in position: {position}</div></> | ||||
|         <PromoteJob item={item} sub={sub} storageKeyPrefix={storageKeyPrefix} /> | ||||
|         {item && <StatusControl item={item} />} | ||||
|         <SubmitButton variant='secondary' className='mt-3'>{item ? 'save' : 'post'}</SubmitButton> | ||||
|       </Form> | ||||
| @ -221,6 +184,61 @@ export default function JobForm ({ item, sub }) { | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function PromoteJob ({ item, sub, storageKeyPrefix }) { | ||||
|   const [monthly, setMonthly] = useState(satsMin2Mo(item?.maxBid || 0)) | ||||
|   const [getAuctionPosition, { data }] = useLazyQuery(gql` | ||||
|     query AuctionPosition($id: ID, $bid: Int!) { | ||||
|       auctionPosition(sub: "${sub.name}", id: $id, bid: $bid) | ||||
|     }`,
 | ||||
|   { fetchPolicy: 'network-only' }) | ||||
|   const position = data?.auctionPosition | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const initialMaxBid = Number(item?.maxBid || localStorage.getItem(storageKeyPrefix + '-maxBid')) || 0 | ||||
|     getAuctionPosition({ variables: { id: item?.id, bid: initialMaxBid } }) | ||||
|     setMonthly(satsMin2Mo(initialMaxBid)) | ||||
|   }, []) | ||||
| 
 | ||||
|   return ( | ||||
|     <AccordianItem | ||||
|       show={item?.maxBid > 0} | ||||
|       header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>promote</div>} | ||||
|       body={ | ||||
|         <> | ||||
|           <Input | ||||
|             label={ | ||||
|               <div className='d-flex align-items-center'>bid | ||||
|                 <Info> | ||||
|                   <ol className='font-weight-bold'> | ||||
|                     <li>The higher your bid the higher your job will rank</li> | ||||
|                     <li>You can increase, decrease, or remove your bid at anytime</li> | ||||
|                     <li>You can edit or stop your job at anytime</li> | ||||
|                     <li>If you run out of sats, your job will stop being promoted until you fill your wallet again</li> | ||||
|                   </ol> | ||||
|                 </Info> | ||||
|                 <small className='text-muted ml-2'>optional</small> | ||||
|               </div> | ||||
|           } | ||||
|             name='maxBid' | ||||
|             onChange={async (formik, e) => { | ||||
|               if (e.target.value >= 0 && e.target.value <= 100000000) { | ||||
|                 setMonthly(satsMin2Mo(e.target.value)) | ||||
|                 getAuctionPosition({ variables: { id: item?.id, bid: Number(e.target.value) } }) | ||||
|               } else { | ||||
|                 setMonthly(satsMin2Mo(0)) | ||||
|               } | ||||
|             }} | ||||
|             append={<InputGroup.Text className='text-monospace'>sats/min</InputGroup.Text>} | ||||
|             hint={<PriceHint monthly={monthly} />} | ||||
|             storageKeyPrefix={storageKeyPrefix} | ||||
|           /> | ||||
|           <><div className='font-weight-bold text-muted'>This bid puts your job in position: {position}</div></> | ||||
|         </> | ||||
|   } | ||||
|     /> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function StatusControl ({ item }) { | ||||
|   let StatusComp | ||||
| 
 | ||||
| @ -241,7 +259,7 @@ function StatusControl ({ item }) { | ||||
|         </> | ||||
|       ) | ||||
|     } | ||||
|   } else { | ||||
|   } else if (item.status === 'STOPPED') { | ||||
|     StatusComp = () => { | ||||
|       return ( | ||||
|         <AccordianItem | ||||
| @ -258,12 +276,13 @@ function StatusControl ({ item }) { | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className='my-2'> | ||||
|       {item.status === 'NOSATS' && | ||||
|         <div className='text-danger font-weight-bold my-1'> | ||||
|           you have no sats! <Link href='/wallet?type=fund' passHref><a className='text-reset text-underline'>fund your wallet</a></Link> to resume your job | ||||
|         </div>} | ||||
|       <StatusComp /> | ||||
|     <div className='my-3 border border-3 rounded'> | ||||
|       <div className='p-3'> | ||||
|         <BootstrapForm.Label>job control</BootstrapForm.Label> | ||||
|         {item.status === 'NOSATS' && | ||||
|           <Alert variant='warning'>your promotion ran out of sats. <Link href='/wallet?type=fund' passHref><a className='text-reset text-underline'>fund your wallet</a></Link> or reduce bid to continue promoting your job</Alert>} | ||||
|         <StatusComp /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -105,13 +105,15 @@ function Notification ({ n }) { | ||||
|                     you were mentioned in | ||||
|                   </small>} | ||||
|                 {n.__typename === 'JobChanged' && | ||||
|                   <small className={`font-weight-bold text-${n.item.status === 'NOSATS' ? 'danger' : 'success'} ml-1`}> | ||||
|                     {n.item.status === 'NOSATS' | ||||
|                       ? 'your job ran out of sats' | ||||
|                       : 'your job is active again'} | ||||
|                   <small className={`font-weight-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ml-1`}> | ||||
|                     {n.item.status === 'ACTIVE' | ||||
|                       ? 'your job is active again' | ||||
|                       : (n.item.status === 'NOSATS' | ||||
|                           ? 'your job promotion ran out of sats' | ||||
|                           : 'your job has been stopped')} | ||||
|                   </small>} | ||||
|                 <div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}> | ||||
|                   {n.item.maxBid | ||||
|                   {n.item.isJob | ||||
|                     ? <ItemJob item={n.item} /> | ||||
|                     : n.item.title | ||||
|                       ? <Item item={n.item} /> | ||||
|  | ||||
| @ -28,6 +28,7 @@ export const ITEM_FIELDS = gql` | ||||
|     commentSats | ||||
|     lastCommentAt | ||||
|     maxBid | ||||
|     isJob | ||||
|     company | ||||
|     location | ||||
|     remote | ||||
|  | ||||
| @ -14,7 +14,7 @@ export default function PostEdit ({ data: { item } }) { | ||||
| 
 | ||||
|   return ( | ||||
|     <LayoutCenter sub={item.sub?.name}> | ||||
|       {item.maxBid | ||||
|       {item.isJob | ||||
|         ? <JobForm item={item} sub={item.sub} /> | ||||
|         : (item.url | ||||
|             ? <LinkForm item={item} editThreshold={editThreshold} adv /> | ||||
|  | ||||
| @ -6,7 +6,7 @@ import { getGetServerSideProps } from '../../../api/ssrApollo' | ||||
| import { useQuery } from '@apollo/client' | ||||
| 
 | ||||
| export const getServerSideProps = getGetServerSideProps(ITEM_FULL, null, | ||||
|   data => !data.item || (data.item.status !== 'ACTIVE' && !data.item.mine)) | ||||
|   data => !data.item || (data.item.status === 'STOPPED' && !data.item.mine)) | ||||
| 
 | ||||
| export default function AnItem ({ data: { item } }) { | ||||
|   const { data } = useQuery(ITEM_FULL, { | ||||
|  | ||||
							
								
								
									
										101
									
								
								prisma/migrations/20220929183848_job_funcs/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								prisma/migrations/20220929183848_job_funcs/migration.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | ||||
| -- charge the user for the auction item | ||||
| CREATE OR REPLACE FUNCTION run_auction(item_id INTEGER) RETURNS void AS $$ | ||||
|     DECLARE | ||||
|         bid INTEGER; | ||||
|         user_id INTEGER; | ||||
|         user_msats INTEGER; | ||||
|         item_status "Status"; | ||||
|         status_updated_at timestamp(3); | ||||
|     BEGIN | ||||
|         PERFORM ASSERT_SERIALIZED(); | ||||
| 
 | ||||
|         -- extract data we need | ||||
|         SELECT "maxBid" * 1000, "userId", status, "statusUpdatedAt" INTO bid, user_id, item_status, status_updated_at FROM "Item" WHERE id = item_id; | ||||
|         SELECT msats INTO user_msats FROM users WHERE id = user_id; | ||||
| 
 | ||||
|         -- 0 bid items expire after 30 days unless updated | ||||
|         IF bid = 0 THEN | ||||
|             IF item_status <> 'STOPPED' AND status_updated_at < now_utc() - INTERVAL '30 days' THEN | ||||
|                 UPDATE "Item" SET status = 'STOPPED', "statusUpdatedAt" = now_utc() WHERE id = item_id; | ||||
|             END IF; | ||||
|             RETURN; | ||||
|         END IF; | ||||
| 
 | ||||
|         -- check if user wallet has enough sats | ||||
|         IF bid > user_msats THEN | ||||
|             -- if not, set status = NOSATS and statusUpdatedAt to now_utc if not already set | ||||
|             IF item_status <> 'NOSATS' THEN | ||||
|                 UPDATE "Item" SET status = 'NOSATS', "statusUpdatedAt" = now_utc() WHERE id = item_id; | ||||
|             END IF; | ||||
|         ELSE | ||||
|             -- if so, deduct from user | ||||
|             UPDATE users SET msats = msats - bid WHERE id = user_id; | ||||
| 
 | ||||
|             -- create an item act | ||||
|             INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at) | ||||
|             VALUES (bid / 1000, item_id, user_id, 'STREAM', now_utc(), now_utc()); | ||||
| 
 | ||||
|             -- update item status = ACTIVE and statusUpdatedAt = now_utc if NOSATS | ||||
|             IF item_status = 'NOSATS' THEN | ||||
|                 UPDATE "Item" SET status = 'ACTIVE', "statusUpdatedAt" = now_utc() WHERE id = item_id; | ||||
|             END IF; | ||||
|         END IF; | ||||
|     END; | ||||
| $$ LANGUAGE plpgsql; | ||||
| 
 | ||||
| -- when creating free item, set freebie flag so can be optionally viewed | ||||
| CREATE OR REPLACE FUNCTION create_job( | ||||
|     title TEXT, url TEXT, text TEXT, user_id INTEGER, job_bid INTEGER, job_company TEXT, | ||||
|     job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER) | ||||
| RETURNS "Item" | ||||
| LANGUAGE plpgsql | ||||
| AS $$ | ||||
| DECLARE | ||||
|     item "Item"; | ||||
| BEGIN | ||||
|     PERFORM ASSERT_SERIALIZED(); | ||||
|     -- create item | ||||
|     SELECT * INTO item FROM create_item(title, url, text, 0, NULL, user_id, NULL, '0'); | ||||
| 
 | ||||
|     -- update by adding additional fields | ||||
|     UPDATE "Item" | ||||
|     SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id, "subName" = 'jobs' | ||||
|     WHERE id = item.id RETURNING * INTO item; | ||||
| 
 | ||||
|     -- run_auction | ||||
|     EXECUTE run_auction(item.id); | ||||
| 
 | ||||
|     RETURN item; | ||||
| END; | ||||
| $$; | ||||
| 
 | ||||
| CREATE OR REPLACE FUNCTION update_job(item_id INTEGER, | ||||
|     item_title TEXT, item_url TEXT, item_text TEXT, job_bid INTEGER, job_company TEXT, | ||||
|     job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER, job_status "Status") | ||||
| RETURNS "Item" | ||||
| LANGUAGE plpgsql | ||||
| AS $$ | ||||
| DECLARE | ||||
|     user_msats INTEGER; | ||||
|     item "Item"; | ||||
| BEGIN | ||||
|     PERFORM ASSERT_SERIALIZED(); | ||||
|     -- update item | ||||
|     SELECT * INTO item FROM update_item(item_id, item_title, item_url, item_text, 0, NULL); | ||||
| 
 | ||||
|     IF item.status <> job_status THEN | ||||
|         UPDATE "Item" | ||||
|         SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id, status = job_status, "statusUpdatedAt" = now_utc() | ||||
|         WHERE id = item.id RETURNING * INTO item; | ||||
|     ELSE | ||||
|         UPDATE "Item" | ||||
|         SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id | ||||
|         WHERE id = item.id RETURNING * INTO item; | ||||
|     END IF; | ||||
| 
 | ||||
|     -- run_auction | ||||
|     EXECUTE run_auction(item.id); | ||||
| 
 | ||||
|     RETURN item; | ||||
| END; | ||||
| $$; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user