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

feature: direct referral system #33

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3e0f1a3
feat: create referral middleware
AvilaAndre Jan 7, 2025
ca1b2ee
fix: move middleware from kernel to routes
AvilaAndre Jan 7, 2025
477bf41
feat: name home path
AvilaAndre Jan 8, 2025
95ad174
feat: referral service
AvilaAndre Jan 8, 2025
f332dab
feat: has_referral_link mixin
AvilaAndre Jan 8, 2025
e186b30
feat: added referral link to user
AvilaAndre Jan 8, 2025
e9bf0fe
feat: add hashids dependency
AvilaAndre Jan 8, 2025
2a30fd1
fix: commit pnpm-lock.yaml
AvilaAndre Jan 8, 2025
7a62395
Merge branch 'main' into feature/referrals
limwa Jan 19, 2025
12c3191
refactor: add type to HashIdService
AvilaAndre Jan 22, 2025
6c1b185
Merge branch 'feature/referrals' of github.com:NIAEFEUP/enei into fea…
AvilaAndre Jan 22, 2025
1276c0f
Merge branch 'develop' into feature/referrals
tomaspalma Feb 7, 2025
5557377
feat: initial version of ReferralService started modelling participan…
tomaspalma Feb 7, 2025
faa787f
feat: Giving out points
AvilaAndre Feb 9, 2025
f2224b7
feat: implement transactions and info migrations
AvilaAndre Feb 9, 2025
08225ef
feat: point attribution and referral by promoter
AvilaAndre Feb 10, 2025
8e8714b
Merge branch 'develop' into feature/referrals
AvilaAndre Feb 10, 2025
447289c
chore: update pnpm-lock after merge
AvilaAndre Feb 10, 2025
129d1e1
fix: remove user_id from info models
AvilaAndre Feb 10, 2025
9faf973
chore: add postgres to the development docker compose
AvilaAndre Feb 11, 2025
c4f26f4
feat: use participant profile as info and create migrations
AvilaAndre Feb 11, 2025
dd9fe8f
feat: store referral user
AvilaAndre Feb 11, 2025
b2e57ab
feat: avoid duplicated referral
AvilaAndre Feb 12, 2025
f1d52b5
fix: direct referrals awards more points
AvilaAndre Feb 12, 2025
3359d40
feat: add frontend for referrals
limwa Feb 14, 2025
f168163
chore: add postgreSQL config variables to env example
AvilaAndre Feb 14, 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
13 changes: 12 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ services:
- valkey-data:/data
ports:
- "6379:6379"
postgres:
image: postgres:latest
ports:
- "${POSTGRES_PORT}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}

volumes:
valkey-data:
valkey-data:
postgres-data:
8 changes: 7 additions & 1 deletion website/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,10 @@ INERTIA_PUBLIC_TZ=Europe/Lisbon
INERTIA_PUBLIC_EVENT_COUNTDOWN_DATE=2025-04-11

# Tuyau
INERTIA_PUBLIC_APP_URL=http://127.0.0.1:3333
INERTIA_PUBLIC_APP_URL=http://127.0.0.1:3333

DB_CONNECTION=postgres
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=
POSTGRES_DB=postgres
34 changes: 34 additions & 0 deletions website/app/controllers/referrals_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { HttpContext } from '@adonisjs/core/http'
import { referralCodeCookie } from '../cookies/referrals_cookies.js'
import ReferralService from '#services/referral_service'
import { inject } from '@adonisjs/core'

@inject()
export default class ReferralsController {
constructor(private referralService: ReferralService) {}

async showReferralLink(ctx: HttpContext) {
const user = ctx.auth.getUserOrFail()
const referralLink = await this.referralService.getReferralLink(user)

return ctx.inertia.render('referrals', { referralLink })
}

async link(ctx: HttpContext) {
const referralCode = ctx.params.referralCode as string
const referrer = await this.referralService.getReferrerByCode(referralCode)

ctx.logger.debug(referrer, "Referrer with code %s", referralCode)

if (referrer) {
const user = ctx.auth.user
if (user) {
await this.referralService.linkUserToReferrer(user, referrer)
} else {
referralCodeCookie.set(ctx, referralCode, { maxAge: 7*24*3600 })
}
}

return ctx.response.redirect().toRoute('pages:home')
}
}
3 changes: 3 additions & 0 deletions website/app/cookies/referrals_cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { TypedCookie } from "#lib/adonisjs/cookies.js";

export const referralCodeCookie = new TypedCookie<string>('referrer')
21 changes: 20 additions & 1 deletion website/app/jobs/update_order_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Job } from 'adonisjs-jobs'
import ConfirmPaymentNotification from '#mails/confirm_payment_notification'
import mail from '@adonisjs/mail/services/main'
import db from '@adonisjs/lucid/services/db'
import app from '@adonisjs/core/services/app'
import User from '#models/user'

type UpdateOrderStatusPayload = {
requestId: string
Expand All @@ -29,13 +31,18 @@ export default class UpdateOrderStatus extends Job {
this.logger.info(`Order status is no longer pending: ${order.status}`)
return // Exit if the status is no longer "Pending"
}

const apiResponse = await axios.get(
`https://api.ifthenpay.com/spg/payment/mbway/status?mbWayKey=${env.get('IFTHENPAY_MBWAY_KEY')}&requestId=${requestId}`
)

if (apiResponse.status === 200) {
const status = apiResponse.data.Message
let status = apiResponse.data.Message
if (status) {
if (app.inDev) {
status = "Success"
}

if (status === 'Pending') {
await UpdateOrderStatus.dispatch({ requestId, email }, { delay: 10000 }) // Retry after 5 seconds
this.logger.info(`Requeued job for requestId: ${requestId}`)
Expand All @@ -54,7 +61,19 @@ export default class UpdateOrderStatus extends Job {

const total = order.total
const orderId = order.id

await mail.send(new ConfirmPaymentNotification(email, products, total, orderId))

const user = await User.find(order.userId)
if (user) {
await user.load('participantProfile')
const participantProfile = user.participantProfile
if (participantProfile) {
// FIXME - this is a hack
participantProfile.purchasedTicket = 'early-bird-with-housing'
await participantProfile.save()
}
}
}
} else {
await UpdateOrderStatus.dispatch({ requestId, email }, { delay: 10000 }) // Retry after 5 seconds
Expand Down
30 changes: 30 additions & 0 deletions website/app/middleware/link_to_user_middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import { referralCodeCookie } from '../cookies/referrals_cookies.js'
import ReferralService from '#services/referral_service'
import { inject } from '@adonisjs/core'

@inject()
export default class LinkToUserMiddleware {
constructor(private referralService: ReferralService) {}

async handle(ctx: HttpContext, next: NextFn) {
const referralCode = referralCodeCookie.get(ctx)

if (referralCode) {
if (!ctx.auth.authenticationAttempted) await ctx.auth.check()

const user = ctx.auth.user
if (user) {
referralCodeCookie.clear(ctx)

const referrer = await this.referralService.getReferrerByCode(referralCode)
if (referrer) {
await this.referralService.linkUserToReferrer(user, referrer)
}
}
}

return next()
}
}
4 changes: 4 additions & 0 deletions website/app/models/participant_profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export default class ParticipantProfile extends BaseModel {
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime

// Ticket Info
@column()
declare purchasedTicket: "early-bird-without-housing" | "early-bird-with-housing" | null

// General Info

@column()
Expand Down
18 changes: 18 additions & 0 deletions website/app/models/promoter_profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DateTime } from 'luxon'
import { BaseModel, column, hasOne } from '@adonisjs/lucid/orm'
import User from './user.js'
import type { HasOne } from '@adonisjs/lucid/types/relations'

export default class PromoterProfile extends BaseModel {
@column({ isPrimary: true })
declare id: number

@hasOne(() => User)
declare user: HasOne<typeof User>

@column.dateTime({ autoCreate: true })
declare createdAt: DateTime

@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
68 changes: 65 additions & 3 deletions website/app/models/user.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { DateTime } from 'luxon'
import { BaseModel, belongsTo, column, hasMany } from '@adonisjs/lucid/orm'
import ParticipantProfile from './participant_profile.js'
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations'
import Account from './account.js'
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations'
import PromoterProfile from './promoter_profile.js'
import ParticipantProfile from './participant_profile.js'

export default class User extends BaseModel {
@column({ isPrimary: true })
Expand All @@ -23,7 +24,33 @@ export default class User extends BaseModel {
@hasMany(() => Account)
declare accounts: HasMany<typeof Account>

// Profiles
// Referrals

@column()
declare referringPromoterId: number | null

@belongsTo(() => User, {
foreignKey: 'promoterId',
})
declare referringPromoter: BelongsTo<typeof User>

@column()
declare referrerId: number | null

@belongsTo(() => User, {
foreignKey: 'referrerId',
})
declare referrer: BelongsTo<typeof User>

// PromoterInfo

@column()
declare promoterProfileId: number | null

@belongsTo(() => PromoterProfile)
declare promoterProfile: BelongsTo<typeof PromoterProfile>

// ParticipantProfile

@column()
declare participantProfileId: number | null
Expand All @@ -33,7 +60,42 @@ export default class User extends BaseModel {

// Functions

get role() {
if (this.isParticipant()) return 'participant' as const
if (this.isPromoter()) return 'promoter' as const
return 'unknown' as const
}

isPromoter() {
return this.promoterProfileId !== null
}

isParticipant() {
return this.participantProfileId !== null
}

isEmailVerified() {
return this.emailVerifiedAt !== null
}

wasReferred() {
return this.referrerId !== null
}

static async hasPurchasedTicket(user: User) {
if (!user.isParticipant()) return false

await user.load('participantProfile')
return !!user.participantProfile.purchasedTicket
}

static async getReferringPromoter(user: User) {
await user.load('referringPromoter')
return user.referringPromoter
}

static async getReferrer(user: User) {
await user.load('referrer')
return user.referrer
}
}
Loading