Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(payments/gifts): start new gift voucher code #966

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
29f9334
feat(payments/gifts): start new gift voucher code
hdahlheim Nov 25, 2024
906052b
feat(payments/gifts): allow monthly to yearly upgreade
hdahlheim Nov 27, 2024
a78f546
feat(payments/gifts): use Crockford Base32 for vouchers
hdahlheim Nov 28, 2024
3dcf1f5
feat(payments/gifts): apply gift to pledge abo
hdahlheim Nov 29, 2024
e74ee7b
feat(payments/gifts): allow gift purchase
hdahlheim Nov 29, 2024
1d2781f
Merge branch 'main' into feat/new-gift-subscriptions
hdahlheim Nov 29, 2024
b75eb9f
fix(payments/gifts): add missing PromotionItemOrder import
hdahlheim Nov 29, 2024
ddd22c5
fix(payments/gift): typescript build error
hdahlheim Dec 2, 2024
19c058d
fix(payments/gifts): rename PromotionItems -> PromotionItem
hdahlheim Dec 2, 2024
d23f935
fix(payments/shop): only return successfully fetched offers
hdahlheim Dec 2, 2024
2fedcaa
chore(payments/gifts): static base32 codes for testing
hdahlheim Dec 2, 2024
cfff144
feat(payments/shop): return offers as interface
hdahlheim Dec 6, 2024
39f0726
fix(payments/shop): promotionItems -> complimentaryItems
hdahlheim Dec 6, 2024
1ae6d49
fix(payments/shop): add offer interface type resolver
hdahlheim Dec 6, 2024
595c629
refactor: add connection context to all queue workers
hdahlheim Dec 11, 2024
b241baf
test(queue): update queue test to new consturctor interface
hdahlheim Dec 12, 2024
9d245f9
feat: add gift voucher checkout handling and store voucher in db
hdahlheim Dec 12, 2024
15a5a6e
chore(shop/gifts): add gift purchase confirmation email
hdahlheim Dec 27, 2024
42273a9
feat(payments/gifts): add gift redemption for YEARLY_ABO
hdahlheim Jan 2, 2025
a3b4765
feat(shop): serialize mail settings into metadata
hdahlheim Jan 2, 2025
79f7bf9
feat(shop/gift): handle pledge monthly abo upgrade
hdahlheim Jan 3, 2025
895d68a
chore(payments/gql): cleanup redeemGiftVoucher mutation
hdahlheim Jan 3, 2025
4615d42
refactor(payments/gifts): giftshop class
hdahlheim Jan 3, 2025
018be8a
refactor(payments): disable confrim:cancel mail
hdahlheim Jan 3, 2025
fb82b95
fix(payments/gifts): add missing mail setting flag
hdahlheim Jan 3, 2025
3859a55
refactor(payments/gifts): cleanup customer creation
hdahlheim Jan 3, 2025
304a3c7
Merge branch 'main' into feat/new-gift-subscriptions
hdahlheim Jan 6, 2025
57a7611
Merge branch 'main' into feat/new-gift-subscriptions
jstcki Jan 7, 2025
857d804
feat(payments/gifts): add validateGiftVoucher resolver
hdahlheim Jan 9, 2025
b07c844
fix(shop/gifts): make sure new voucehr has not been redeemed
hdahlheim Jan 9, 2025
a03f946
feat(payments/gifts): make 3 months gift work for yearly subs
hdahlheim Jan 10, 2025
41b89bc
fix(payments/mailsettings): copy base mail settings
hdahlheim Jan 13, 2025
1b1ab94
chore(payments/gifts): change behavior of three months for yearly
hdahlheim Jan 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions apps/api/server.js
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ const {
SyncMailchimpSetupWorker,
SyncMailchimpUpdateWorker,
SyncMailchimpEndedWorker,
setupPaymentUserEventHooks,
} = require('@orbiting/backend-modules-payments')

const loaderBuilders = {
@@ -81,20 +82,31 @@ const MailScheduler = require('@orbiting/backend-modules-mail/lib/scheduler')

const mail = require('@orbiting/backend-modules-republik-crowdfundings/lib/Mail')

const { Queue } = require('@orbiting/backend-modules-job-queue')

const queue = Queue.getInstance()
queue.registerWorker(StripeWebhookWorker)
queue.registerWorker(StripeCustomerCreateWorker)
queue.registerWorker(SyncAddressDataWorker)
queue.registerWorker(ConfirmSetupTransactionalWorker)
queue.registerWorker(ConfirmCancelTransactionalWorker)
queue.registerWorker(ConfirmRevokeCancellationTransactionalWorker)
queue.registerWorker(NoticeEndedTransactionalWorker)
queue.registerWorker(NoticePaymentFailedTransactionalWorker)
queue.registerWorker(SyncMailchimpSetupWorker)
queue.registerWorker(SyncMailchimpUpdateWorker)
queue.registerWorker(SyncMailchimpEndedWorker)
const { Queue, GlobalQueue } = require('@orbiting/backend-modules-job-queue')

function setupQueue(context, monitorQueueState = undefined) {
const queue = Queue.createInstance(GlobalQueue, {
context,
connectionString: process.env.DATABASE_URL,
monitorStateIntervalSeconds: monitorQueueState,
})

queue.registerWorkers([
StripeWebhookWorker,
StripeCustomerCreateWorker,
SyncAddressDataWorker,
ConfirmSetupTransactionalWorker,
ConfirmCancelTransactionalWorker,
ConfirmRevokeCancellationTransactionalWorker,
NoticeEndedTransactionalWorker,
NoticePaymentFailedTransactionalWorker,
SyncMailchimpSetupWorker,
SyncMailchimpUpdateWorker,
SyncMailchimpEndedWorker,
])

return queue
}

const {
LOCAL_ASSETS_SERVER,
@@ -204,7 +216,9 @@ const run = async (workerId, config) => {

const connectionContext = await ConnectionContext.create(applicationName)

const queue = setupQueue(connectionContext)
await queue.start()
setupPaymentUserEventHooks(connectionContext)

const createGraphQLContext = (defaultContext) => {
const loaders = {}
@@ -356,6 +370,7 @@ const runOnce = async () => {
)
}

const queue = setupQueue(connectionContext, 120)
await queue.start()

PaymentsService.start(context.pgdb)
17 changes: 9 additions & 8 deletions packages/backend-modules/job-queue/__test__/queue.test.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import {
} from '@testcontainers/postgresql'
import { JobState } from '../lib/types'
import { Job, SendOptions } from 'pg-boss'
import { ConnectionContext } from '@orbiting/backend-modules-types'

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

@@ -40,20 +41,20 @@ describe('pg-boss worker test', () => {
beforeAll(async () => {
postgresContainer = await new PostgreSqlContainer().start()

queue = new Queue({
application_name: 'job-queue',
connectionString: postgresContainer.getConnectionUri(),
})
queue = new Queue(
{
application_name: 'job-queue',
connectionString: postgresContainer.getConnectionUri(),
},
// mock connection context because we dont use it in this test
{} as ConnectionContext,
)
queue.registerWorker(DemoWorker).registerWorker(DemoErrorWorker)

await queue.start()
await queue.startWorkers()
}, 60000)

beforeEach(async () => {
// console.log(await queue.getQueues())
})

afterAll(async () => {
await queue.stop()
}, 30000)
61 changes: 48 additions & 13 deletions packages/backend-modules/job-queue/lib/queue.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,56 @@
import PgBoss from 'pg-boss'
import { Worker, WorkerJobArgs, WorkerQueueName } from './types'
import { getConfig } from './config'
import { ConnectionContext } from '@orbiting/backend-modules-types'

export const GlobalQueue = Symbol('Global PGBoss queue')

type WorkerConstructor = new (
pgBoss: PgBoss,
context: ConnectionContext,
) => Worker<any>

export class Queue {
static instance: Queue
static instances: Record<symbol, Queue> = {}

protected readonly pgBoss: PgBoss
protected readonly context: ConnectionContext
protected workers = new Map<WorkerQueueName<Worker<any>>, Worker<any>>()

static getInstance(): Queue {
if (!this.instance) {
const config = getConfig()
this.instance = new Queue({
application_name: config.queueApplicationName,
static createInstance(
id: symbol = GlobalQueue,
config: {
connectionString: string
monitorStateIntervalSeconds?: number
context: ConnectionContext
},
) {
this.instances[id] = new Queue(
{
application_name: id.description,
connectionString: config.connectionString,
monitorStateIntervalSeconds: 120,
})
monitorStateIntervalSeconds: config.monitorStateIntervalSeconds,
},
config.context,
)

return this.instances[id]
}

static getInstance(id: symbol = GlobalQueue): Queue {
if (!this.instances[id]) {
throw new Error('Unknown queue instance')
}

return this.instance
return this.instances[id]
}

constructor(options: PgBoss.ConstructorOptions) {
constructor(options: PgBoss.ConstructorOptions, context: ConnectionContext) {
if (typeof options.monitorStateIntervalSeconds === 'undefined') {
delete options.monitorStateIntervalSeconds
}

this.pgBoss = new PgBoss(options)
this.context = context

this.pgBoss.on('error', (error) => {
console.error('[JobQueue]: %s', error)
@@ -32,12 +60,19 @@ export class Queue {
})
}

registerWorker(worker: new (pgBoss: PgBoss) => Worker<any>): Queue {
const workerInstance = new worker(this.pgBoss)
registerWorker(worker: WorkerConstructor): Queue {
const workerInstance = new worker(this.pgBoss, this.context)
this.workers.set(workerInstance.queue, workerInstance)
return this
}

registerWorkers(workers: WorkerConstructor[]): Queue {
for (const worker of workers) {
this.registerWorker(worker)
}
return this
}

async start() {
await this.pgBoss.start()

5 changes: 4 additions & 1 deletion packages/backend-modules/job-queue/lib/workers/base.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import PgBoss, { Job, ScheduleOptions, SendOptions } from 'pg-boss'
import { BasePayload, Worker, WorkerQueue } from '../types'
import { ConnectionContext } from '@orbiting/backend-modules-types'

export abstract class BaseWorker<T extends Omit<BasePayload, '$version'>>
implements Worker<T>
{
protected pgBoss: PgBoss
protected readonly context: ConnectionContext
abstract readonly queue: WorkerQueue
readonly options: SendOptions = { retryLimit: 3, retryDelay: 1000 }
// abstract performOptions?: PgBoss.WorkOptions | undefined

constructor(pgBoss: PgBoss) {
constructor(pgBoss: PgBoss, context: ConnectionContext) {
this.pgBoss = pgBoss
this.context = context
}

abstract perform(jobs: Job<T>[]): Promise<void>
1 change: 1 addition & 0 deletions packages/backend-modules/job-queue/package.json
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
"main": "build/index.js",
"types": "build/@types",
"dependencies": {
"@orbiting/backend-modules-types": "*",
"pg-boss": "^10.1.1",
"debug": "^4.3.5"
},
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!--[if gte mso 15]>
<xml>
<o:officedocumentsettings>
<o:allowpng />
<o:pixelsperinch>96</o:pixelsperinch>
</o:officedocumentsettings>
</xml>
<![endif]-->
<style type="text/css">
{{{sg_font_faces}}}
</style>
<style type="text/css">
p {
color:#282828;font-size:17px;line-height:24px;
{{{sg_font_style_sans_serif_regular}}}
}
p strong {
{{{sg_font_style_sans_serif_medium}}}
}
</style>
</head>
<body style="margin: 0; padding: 0; background-color: #fff">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tbody>
<tr>
<td align="center" valign="top">
<table
align="center"
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="max-width:640px;color:#282828;font-size:17px;line-height:24px;{{sg_font_style_sans_serif_regular}}"
>
<tbody>
<tr>
<td style="padding: 20px">
<p>Guten Tag</p>
<p>
<strong>
Ihre Zahlung ist erfolgreich bei uns eingegangen.
</strong>
</p>

{{#if voucher_codes}}
<p>
<strong
>Herzlichen Dank, dass Sie die Republik mit einer
Geschenk-Mitgliedschaft unterstützen!</strong
>
</p>
<p>
<strong
>Für die Geschenk-Mitgliedschaft erhalten Sie
nachfolgend einen 8-Zeichen-Code.</strong
>
</p>
<p>
<strong>{{voucher_codes}}</strong>
</p>
<p>
Sie können diesen der beschenkten Person mit einem Mittel
Ihrer Wahl überreichen: sofort per E-Mail, traditionell
per Briefpost oder originell als Schrift auf einem Kuchen.
</p>
<p>
Um den Geschenkcode einzulösen, muss der neue Besitzer
oder die neue Besitzerin nur auf die Seite
<a href="{{link_claim}}">{{link_claim}}</a> gehen. Und ihn
dort eingeben.
</p>
{{/if}}

<ul
style="color:#282828;font-size:17px;line-height:24px;{{sg_font_style_sans_serif_regular}}"
>
{{#options}} {{#if this.isOTypeGoodie}}
<li>{{this.oamount}} {{this.olabel}}</li>
{{/if}} {{/options}}
</ul>

<p>
Vielen Dank! {{#if voucher_codes}}
<br />Und viel Freude beim Verschenken der Republik.
{{/if}}
</p>
<p>Ihre Crew der Republik</p>
</td>
</tr>
<tr>
<td style="padding: 20px">
<p
style="color:#282828;font-size:14px;line-height:20px;{{sg_font_style_sans_serif_regular}}"
>
<img
height="79"
src="{{frontend_base_url}}/static/logo_republik_newsletter.png"
style="
border: 0px;
width: 180px !important;
height: 79px !important;
margin: 0px;
"
width="180"
alt=""
/>
<br />
Republik AG<br />
Sihlhallenstrasse 1, CH-8004 Zürich<br />
<a href="{{frontend_base_url}}">www.republik.ch</a><br />
<a href="mailto:kontakt@republik.ch"
>kontakt@republik.ch</a
>
</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const run = require('../run.js')

const dir = 'packages/backend-modules/payments/migrations/sql'
const file = '20241212140600-alter-order-table-for-gifts'

exports.up = (db) =>
run(db, dir, `${file}-up.sql`)

exports.down = (db) =>
run(db, dir, `${file}-down.sql`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const run = require('../run.js')

const dir = 'packages/backend-modules/payments/migrations/sql'
const file = '20241212144915-gift-code-table'

exports.up = (db) =>
run(db, dir, `${file}-up.sql`)

exports.down = (db) =>
run(db, dir, `${file}-down.sql`)
Loading
Loading