mirror of
https://github.com/MAGICGrants/campaign-site.git
synced 2026-01-08 20:08:05 -05:00
Handle manually marked invoices and display ltc donations
This commit is contained in:
@@ -6,3 +6,4 @@ export const MONTHLY_MEMBERSHIP_MIN_PRICE_USD = 10
|
||||
export const ANNUALLY_MEMBERSHIP_MIN_PRICE_USD = 100
|
||||
export const POINTS_PER_USD = 1
|
||||
export const POINTS_REDEEM_PRICE_USD = 0.1
|
||||
export const NET_DONATION_AMOUNT_WITH_POINTS_RATE = 0.9
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { GetServerSidePropsContext, NextPage } from 'next/types'
|
||||
import Head from 'next/head'
|
||||
@@ -15,7 +13,6 @@ import PageHeading from '../../../components/PageHeading'
|
||||
import Progress from '../../../components/Progress'
|
||||
import { prisma } from '../../../server/services'
|
||||
import { Button } from '../../../components/ui/button'
|
||||
import CustomLink from '../../../components/CustomLink'
|
||||
import { trpc } from '../../../utils/trpc'
|
||||
import { getFundSlugFromUrlPath } from '../../../utils/funds'
|
||||
import { useFundSlug } from '../../../utils/use-fund-slug'
|
||||
@@ -31,10 +28,6 @@ type SingleProjectPageProps = {
|
||||
|
||||
const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) => {
|
||||
const router = useRouter()
|
||||
const [registerIsOpen, setRegisterIsOpen] = useState(false)
|
||||
const [loginIsOpen, setLoginIsOpen] = useState(false)
|
||||
const [passwordResetIsOpen, setPasswordResetIsOpen] = useState(false)
|
||||
const session = useSession()
|
||||
const fundSlug = useFundSlug()
|
||||
|
||||
const { slug, title, summary, coverImage, content, nym, website, goal, isFunded } = project
|
||||
@@ -106,10 +99,13 @@ const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) =
|
||||
|
||||
<ul className="font-semibold space-y-1">
|
||||
<li className="flex items-center space-x-1">
|
||||
<span className="text-green-500 text-xl">{`${formatUsd(donationStats.xmr.fiatAmount + donationStats.btc.fiatAmount + donationStats.usd.fiatAmount)}`}</span>{' '}
|
||||
<span className="text-green-500 text-xl">{`${formatUsd(donationStats.xmr.fiatAmount + donationStats.btc.fiatAmount + donationStats.ltc.fiatAmount + donationStats.manual.fiatAmount + donationStats.usd.fiatAmount)}`}</span>{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in{' '}
|
||||
{donationStats.xmr.count + donationStats.btc.count + donationStats.usd.count}{' '}
|
||||
{donationStats.xmr.count +
|
||||
donationStats.btc.count +
|
||||
donationStats.manual.count +
|
||||
donationStats.usd.count}{' '}
|
||||
donations total
|
||||
</span>
|
||||
</li>
|
||||
@@ -126,9 +122,16 @@ const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) =
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{`${formatUsd(donationStats.usd.amount)}`} Fiat{' '}
|
||||
{donationStats.ltc.amount}LTC{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.usd.count} donations
|
||||
in {donationStats.ltc.count} donations
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
{`${formatUsd(donationStats.usd.amount + donationStats.manual.fiatAmount)}`}{' '}
|
||||
Fiat{' '}
|
||||
<span className="font-normal text-sm text-gray">
|
||||
in {donationStats.usd.count + donationStats.manual.count} donations
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -206,6 +209,11 @@ export async function getServerSideProps({ params, resolvedUrl }: GetServerSideP
|
||||
amount: project.isFunded ? project.totalDonationsLTC : 0,
|
||||
fiatAmount: project.isFunded ? project.totalDonationsLTCInFiat : 0,
|
||||
},
|
||||
manual: {
|
||||
count: project.isFunded ? project.numDonationsManual : 0,
|
||||
amount: project.isFunded ? project.totalDonationsManual : 0,
|
||||
fiatAmount: project.isFunded ? project.totalDonationsManual : 0,
|
||||
},
|
||||
usd: {
|
||||
count: project.isFunded ? project.numDonationsFiat : 0,
|
||||
amount: project.isFunded ? project.totalDonationsFiat : 0,
|
||||
@@ -222,6 +230,7 @@ export async function getServerSideProps({ params, resolvedUrl }: GetServerSideP
|
||||
BTC: donationStats.btc,
|
||||
XMR: donationStats.xmr,
|
||||
LTC: donationStats.ltc,
|
||||
MANUAL: donationStats.manual,
|
||||
} as const
|
||||
|
||||
donations.forEach((donation) => {
|
||||
|
||||
@@ -13,10 +13,14 @@ import { btcpayApi as _btcpayApi, btcpayApi, prisma } from '../../../server/serv
|
||||
import { env } from '../../../env.mjs'
|
||||
import { givePointsToUser } from '../../../server/utils/perks'
|
||||
import { sendDonationConfirmationEmail } from '../../../server/utils/mailing'
|
||||
import { POINTS_PER_USD } from '../../../config'
|
||||
import { NET_DONATION_AMOUNT_WITH_POINTS_RATE, POINTS_PER_USD } from '../../../config'
|
||||
import { getDonationAttestation, getMembershipAttestation } from '../../../server/utils/attestation'
|
||||
import { addUserToPgMembersGroup } from '../../../utils/pg-forum-connection'
|
||||
import { log } from '../../../utils/logging'
|
||||
import {
|
||||
getBtcPayInvoice,
|
||||
getBtcPayInvoicePaymentMethods,
|
||||
} from '../../../server/utils/btcpayserver'
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@@ -25,6 +29,7 @@ export const config = {
|
||||
}
|
||||
|
||||
type WebhookBody = Record<string, any> & {
|
||||
manuallyMarked: boolean
|
||||
deliveryId: string
|
||||
webhookId: string
|
||||
originalDeliveryId: string
|
||||
@@ -72,11 +77,7 @@ async function handleFundingRequiredApiDonation(body: WebhookBody) {
|
||||
|
||||
// This handles both donations and memberships.
|
||||
async function handleDonationOrMembership(body: WebhookBody) {
|
||||
if (!body.metadata) return
|
||||
|
||||
const { data: paymentMethods } = await btcpayApi.get<BtcPayGetPaymentMethodsRes>(
|
||||
`/invoices/${body.invoiceId}/payment-methods`
|
||||
)
|
||||
if (!body.metadata || JSON.stringify(body.metadata) === '{}') return
|
||||
|
||||
const termToMembershipExpiresAt = {
|
||||
monthly: dayjs().add(1, 'month').toDate(),
|
||||
@@ -90,17 +91,20 @@ async function handleDonationOrMembership(body: WebhookBody) {
|
||||
}
|
||||
|
||||
const cryptoPayments: DonationCryptoPayments = []
|
||||
const paymentMethods = await getBtcPayInvoicePaymentMethods(body.invoiceId)
|
||||
const shouldGivePointsBack = body.metadata.givePointsBack === 'true'
|
||||
|
||||
// Get how much was paid for each crypto
|
||||
paymentMethods.forEach((paymentMethod) => {
|
||||
if (!body.metadata) return
|
||||
|
||||
const shouldGivePointsBack = body.metadata.givePointsBack === 'true'
|
||||
const cryptoRate = Number(paymentMethod.rate)
|
||||
const grossCryptoAmount = Number(paymentMethod.paymentMethodPaid)
|
||||
|
||||
// Deduct 10% of amount if donator wants points
|
||||
const netCryptoAmount = shouldGivePointsBack ? grossCryptoAmount * 0.9 : grossCryptoAmount
|
||||
const netCryptoAmount = shouldGivePointsBack
|
||||
? grossCryptoAmount * NET_DONATION_AMOUNT_WITH_POINTS_RATE
|
||||
: grossCryptoAmount
|
||||
|
||||
// Move on if amound paid with current method is 0
|
||||
if (!grossCryptoAmount) return
|
||||
@@ -113,17 +117,40 @@ async function handleDonationOrMembership(body: WebhookBody) {
|
||||
})
|
||||
})
|
||||
|
||||
const grossFiatAmount = Object.values(cryptoPayments).reduce(
|
||||
// Handle marked paid invoice
|
||||
if (body.manuallyMarked) {
|
||||
const invoice = await getBtcPayInvoice(body.invoiceId)
|
||||
|
||||
const amountPaidFiat = cryptoPayments.reduce(
|
||||
(total, paymentMethod) => total + paymentMethod.grossAmount * paymentMethod.rate,
|
||||
0
|
||||
)
|
||||
|
||||
const invoiceAmountFiat = Number(invoice.amount)
|
||||
const amountDueFiat = invoiceAmountFiat - amountPaidFiat
|
||||
|
||||
if (amountDueFiat > 0) {
|
||||
cryptoPayments.push({
|
||||
cryptoCode: 'MANUAL',
|
||||
grossAmount: amountDueFiat,
|
||||
netAmount: shouldGivePointsBack
|
||||
? amountDueFiat * NET_DONATION_AMOUNT_WITH_POINTS_RATE
|
||||
: amountDueFiat,
|
||||
rate: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const grossFiatAmount = cryptoPayments.reduce(
|
||||
(total, paymentMethod) => total + paymentMethod.grossAmount * paymentMethod.rate,
|
||||
0
|
||||
)
|
||||
|
||||
const netFiatAmount = Object.values(cryptoPayments).reduce(
|
||||
const netFiatAmount = cryptoPayments.reduce(
|
||||
(total, paymentMethod) => total + paymentMethod.netAmount * paymentMethod.rate,
|
||||
0
|
||||
)
|
||||
|
||||
const shouldGivePointsBack = body.metadata.givePointsBack === 'true'
|
||||
const pointsToGive = shouldGivePointsBack ? Math.floor(grossFiatAmount / POINTS_PER_USD) : 0
|
||||
|
||||
const donation = await prisma.donation.create({
|
||||
|
||||
@@ -30,7 +30,7 @@ export type DonationMetadata = {
|
||||
}
|
||||
|
||||
export type DonationCryptoPayments = {
|
||||
cryptoCode: 'BTC' | 'XMR' | 'LTC'
|
||||
cryptoCode: 'BTC' | 'XMR' | 'LTC' | 'MANUAL'
|
||||
grossAmount: number
|
||||
netAmount: number
|
||||
rate: number
|
||||
@@ -44,6 +44,11 @@ export type BtcPayGetRatesRes = [
|
||||
},
|
||||
]
|
||||
|
||||
export type BtcPayGetInvoiceRes = {
|
||||
id: string
|
||||
amount: string
|
||||
}
|
||||
|
||||
export type BtcPayGetPaymentMethodsRes = {
|
||||
rate: string
|
||||
amount: string
|
||||
|
||||
26
server/utils/btcpayserver.ts
Normal file
26
server/utils/btcpayserver.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { log } from '../../utils/logging'
|
||||
import { btcpayApi } from '../services'
|
||||
import { BtcPayGetInvoiceRes, BtcPayGetPaymentMethodsRes } from '../types'
|
||||
|
||||
export async function getBtcPayInvoice(id: string) {
|
||||
try {
|
||||
const { data: invoice } = await btcpayApi.get<BtcPayGetInvoiceRes>(`/invoices/${id}`)
|
||||
return invoice
|
||||
} catch (error) {
|
||||
log('error', `Failed to get BTCPayServer invoice ${id}.`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBtcPayInvoicePaymentMethods(invoiceId: string) {
|
||||
try {
|
||||
const { data: paymentMethods } = await btcpayApi.get<BtcPayGetPaymentMethodsRes>(
|
||||
`/invoices/${invoiceId}/payment-methods`
|
||||
)
|
||||
|
||||
return paymentMethods
|
||||
} catch (error) {
|
||||
log('error', `Failed to get BTCPayServer payment methods for invoice ${invoiceId}.`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { DonationMetadata, StrapiCreatePointBody } from '../../server/types'
|
||||
import { sendDonationConfirmationEmail } from './mailing'
|
||||
import { getPointsBalance, givePointsToUser } from './perks'
|
||||
import { POINTS_PER_USD } from '../../config'
|
||||
import { NET_DONATION_AMOUNT_WITH_POINTS_RATE, POINTS_PER_USD } from '../../config'
|
||||
import { getDonationAttestation, getMembershipAttestation } from './attestation'
|
||||
import { addUserToPgMembersGroup } from '../../utils/pg-forum-connection'
|
||||
import { log } from '../../utils/logging'
|
||||
@@ -32,7 +32,7 @@ async function handleDonationOrNonRecurringMembership(paymentIntent: Stripe.Paym
|
||||
const shouldGivePointsBack = metadata.givePointsBack === 'true'
|
||||
const grossFiatAmount = paymentIntent.amount_received / 100
|
||||
const netFiatAmount = shouldGivePointsBack
|
||||
? Number((grossFiatAmount * 0.9).toFixed(2))
|
||||
? Number((grossFiatAmount * NET_DONATION_AMOUNT_WITH_POINTS_RATE).toFixed(2))
|
||||
: grossFiatAmount
|
||||
const pointsToGive = shouldGivePointsBack ? Math.floor(grossFiatAmount / POINTS_PER_USD) : 0
|
||||
let membershipExpiresAt = null
|
||||
@@ -143,7 +143,7 @@ async function handleRecurringMembership(invoice: Stripe.Invoice) {
|
||||
const shouldGivePointsBack = metadata.givePointsBack === 'true'
|
||||
const grossFiatAmount = invoice.total / 100
|
||||
const netFiatAmount = shouldGivePointsBack
|
||||
? Number((grossFiatAmount * 0.9).toFixed(2))
|
||||
? Number((grossFiatAmount * NET_DONATION_AMOUNT_WITH_POINTS_RATE).toFixed(2))
|
||||
: grossFiatAmount
|
||||
const pointsToGive = shouldGivePointsBack ? parseInt(String(grossFiatAmount * 100)) : 0
|
||||
const membershipExpiresAt = new Date(invoiceLine.period.end * 1000)
|
||||
|
||||
@@ -20,6 +20,7 @@ export const funds: Record<FundSlug, ProjectItem & { slug: FundSlug }> = {
|
||||
numDonationsXMR: 0,
|
||||
numDonationsLTC: 0,
|
||||
numDonationsFiat: 0,
|
||||
numDonationsManual: 0,
|
||||
totalDonationsBTC: 0,
|
||||
totalDonationsXMR: 0,
|
||||
totalDonationsLTC: 0,
|
||||
@@ -27,6 +28,7 @@ export const funds: Record<FundSlug, ProjectItem & { slug: FundSlug }> = {
|
||||
totalDonationsBTCInFiat: 0,
|
||||
totalDonationsXMRInFiat: 0,
|
||||
totalDonationsLTCInFiat: 0,
|
||||
totalDonationsManual: 0,
|
||||
},
|
||||
firo: {
|
||||
fund: 'firo',
|
||||
@@ -45,6 +47,7 @@ export const funds: Record<FundSlug, ProjectItem & { slug: FundSlug }> = {
|
||||
numDonationsXMR: 0,
|
||||
numDonationsLTC: 0,
|
||||
numDonationsFiat: 0,
|
||||
numDonationsManual: 0,
|
||||
totalDonationsBTC: 0,
|
||||
totalDonationsXMR: 0,
|
||||
totalDonationsLTC: 0,
|
||||
@@ -52,6 +55,7 @@ export const funds: Record<FundSlug, ProjectItem & { slug: FundSlug }> = {
|
||||
totalDonationsBTCInFiat: 0,
|
||||
totalDonationsXMRInFiat: 0,
|
||||
totalDonationsLTCInFiat: 0,
|
||||
totalDonationsManual: 0,
|
||||
},
|
||||
privacyguides: {
|
||||
fund: 'privacyguides',
|
||||
@@ -75,6 +79,7 @@ export const funds: Record<FundSlug, ProjectItem & { slug: FundSlug }> = {
|
||||
numDonationsXMR: 0,
|
||||
numDonationsLTC: 0,
|
||||
numDonationsFiat: 0,
|
||||
numDonationsManual: 0,
|
||||
totalDonationsBTC: 0,
|
||||
totalDonationsXMR: 0,
|
||||
totalDonationsLTC: 0,
|
||||
@@ -82,6 +87,7 @@ export const funds: Record<FundSlug, ProjectItem & { slug: FundSlug }> = {
|
||||
totalDonationsBTCInFiat: 0,
|
||||
totalDonationsXMRInFiat: 0,
|
||||
totalDonationsLTCInFiat: 0,
|
||||
totalDonationsManual: 0,
|
||||
},
|
||||
general: {
|
||||
fund: 'general',
|
||||
@@ -104,6 +110,7 @@ export const funds: Record<FundSlug, ProjectItem & { slug: FundSlug }> = {
|
||||
numDonationsXMR: 0,
|
||||
numDonationsLTC: 0,
|
||||
numDonationsFiat: 0,
|
||||
numDonationsManual: 0,
|
||||
totalDonationsBTC: 0,
|
||||
totalDonationsXMR: 0,
|
||||
totalDonationsLTC: 0,
|
||||
@@ -111,6 +118,7 @@ export const funds: Record<FundSlug, ProjectItem & { slug: FundSlug }> = {
|
||||
totalDonationsBTCInFiat: 0,
|
||||
totalDonationsXMRInFiat: 0,
|
||||
totalDonationsLTCInFiat: 0,
|
||||
totalDonationsManual: 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Donation, FundSlug } from '@prisma/client'
|
||||
import { fundSlugs } from './funds'
|
||||
import { ProjectItem } from './types'
|
||||
import { prisma } from '../server/services'
|
||||
import { env } from '../env.mjs'
|
||||
import { DonationCryptoPayments } from '../server/types'
|
||||
|
||||
const directories: Record<FundSlug, string> = {
|
||||
@@ -65,10 +64,12 @@ export function getProjectBySlug(slug: string, fundSlug: FundSlug) {
|
||||
numDonationsXMR: data.numDonationsXMR || 0,
|
||||
numDonationsLTC: data.numDonationsLTC || 0,
|
||||
numDonationsFiat: data.numDonationsFiat || 0,
|
||||
numDonationsManual: data.numDonationsManual || 0,
|
||||
totalDonationsBTC: data.totalDonationsBTC || 0,
|
||||
totalDonationsXMR: data.totalDonationsXMR || 0,
|
||||
totalDonationsLTC: data.totalDonationsLTC || 0,
|
||||
totalDonationsFiat: data.totalDonationsFiat || 0,
|
||||
totalDonationsManual: data.totalDonationsManual || 0,
|
||||
totalDonationsBTCInFiat: data.totalDonationsBTCInFiat || 0,
|
||||
totalDonationsXMRInFiat: data.totalDonationsXMRInFiat || 0,
|
||||
totalDonationsLTCInFiat: data.totalDonationsLTCInFiat || 0,
|
||||
@@ -140,6 +141,11 @@ export async function getProjects(fundSlug?: FundSlug) {
|
||||
project.totalDonationsLTC += payment.netAmount
|
||||
project.totalDonationsLTCInFiat += payment.netAmount * payment.rate
|
||||
}
|
||||
|
||||
if (payment.cryptoCode === 'MANUAL') {
|
||||
project.numDonationsManual += 1
|
||||
project.totalDonationsManual += payment.netAmount * payment.rate
|
||||
}
|
||||
})
|
||||
|
||||
if (!donation.cryptoPayments) {
|
||||
|
||||
@@ -18,10 +18,12 @@ export type ProjectItem = {
|
||||
numDonationsXMR: number
|
||||
numDonationsLTC: number
|
||||
numDonationsFiat: number
|
||||
numDonationsManual: number
|
||||
totalDonationsBTC: number
|
||||
totalDonationsXMR: number
|
||||
totalDonationsLTC: number
|
||||
totalDonationsFiat: number
|
||||
totalDonationsManual: number
|
||||
totalDonationsBTCInFiat: number
|
||||
totalDonationsXMRInFiat: number
|
||||
totalDonationsLTCInFiat: number
|
||||
@@ -46,6 +48,16 @@ export type ProjectDonationStats = {
|
||||
amount: number
|
||||
fiatAmount: number
|
||||
}
|
||||
ltc: {
|
||||
count: number
|
||||
amount: number
|
||||
fiatAmount: number
|
||||
}
|
||||
manual: {
|
||||
count: number
|
||||
amount: number
|
||||
fiatAmount: number
|
||||
}
|
||||
usd: {
|
||||
count: number
|
||||
amount: number
|
||||
|
||||
Reference in New Issue
Block a user