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: payments #14

Merged
merged 69 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
a95bda8
feat: created product and order models
jose-carlos-sousa Dec 21, 2024
59a7808
feat: post order endpoint working
jose-carlos-sousa Dec 21, 2024
a7440ff
feat: initial cart prototype
HenriqueSFernandes Dec 21, 2024
367a5ae
feat: init endpoints
jose-carlos-sousa Dec 21, 2024
746d8d0
feat: improved payment page design
HenriqueSFernandes Dec 21, 2024
0d5f433
feat: initial cron job implementation
jose-carlos-sousa Dec 21, 2024
167a958
fix :removed console logs
jose-carlos-sousa Dec 21, 2024
4f92b36
feat: improved looks, not sure about the border on the images
HenriqueSFernandes Dec 22, 2024
cae5f9e
feat: removed ugly border on mbway and mb icons
HenriqueSFernandes Dec 23, 2024
b369645
feat: added phone number modal
HenriqueSFernandes Dec 23, 2024
4f26dbe
fix: added description to phone modal
HenriqueSFernandes Dec 23, 2024
250d857
fix: now order works for multiple products
jose-carlos-sousa Dec 23, 2024
cf96b01
feat: added endpoint to submit payment
HenriqueSFernandes Dec 23, 2024
c0f5a20
fix: billing address now starts disabled
HenriqueSFernandes Dec 23, 2024
5abc7d2
feat: working multiple products
jose-carlos-sousa Dec 23, 2024
92c2c4e
feat: added error messages to phone modal
HenriqueSFernandes Dec 23, 2024
ddd4489
feat : check max order
jose-carlos-sousa Dec 23, 2024
e5ec4de
chore: user seeder
jose-carlos-sousa Dec 23, 2024
4db010a
Merge branch 'feature/payments-page' into 1-buy-early-bird-ticket-flow
HenriqueSFernandes Dec 23, 2024
b753452
feat: integrated payment frontend with backend
HenriqueSFernandes Dec 23, 2024
309c999
Merge branch 'main' into 1-buy-early-bird-ticket-flow
HenriqueSFernandes Dec 23, 2024
b90dc01
feat : created checks for quantaty bought
jose-carlos-sousa Dec 24, 2024
8a22c4e
feat: now route requires authentication
jose-carlos-sousa Dec 24, 2024
c05e400
chore: small changes to lock
jose-carlos-sousa Dec 24, 2024
16e8651
chore:simulate login frontend
jose-carlos-sousa Dec 24, 2024
ff60a4b
chore: formatted the code
HenriqueSFernandes Dec 25, 2024
a9f9d1b
ignore, this commit tests the linter
HenriqueSFernandes Dec 25, 2024
a1c59ec
fix: renamed all snake_case variables to camelCase
HenriqueSFernandes Dec 25, 2024
bc9c9c0
Merge branch 'main' into 1-buy-early-bird-ticket-flow
HenriqueSFernandes Dec 31, 2024
ec710e8
Merge remote-tracking branch 'origin/fix/no-declaration-for-tuyau-pro…
jose-carlos-sousa Jan 7, 2025
8fd9047
initial mail integration
jose-carlos-sousa Jan 7, 2025
b357de3
Merge remote-tracking branch 'origin/main' into 1-buy-early-bird-tick…
jose-carlos-sousa Jan 18, 2025
eeaa430
small adjustments to frontend
jose-carlos-sousa Jan 19, 2025
8e29bfb
fix. small adjustments to validation
jose-carlos-sousa Jan 19, 2025
9aab3ff
Merge branch 'main' into 1-buy-early-bird-ticket-flow
limwa Jan 19, 2025
3a4b2b5
fix: now tickets in payment page are not hardcoded
jose-carlos-sousa Jan 19, 2025
81f818a
fix: now tickets in payment page are not hardcoded
jose-carlos-sousa Jan 19, 2025
269fe3b
chore: change deployment configuration to handle jobs
limwa Jan 19, 2025
b85829f
Merge remote-tracking branch 'origin/1-buy-early-bird-ticket-flow' in…
jose-carlos-sousa Jan 19, 2025
ae3fea2
fix: fixed error in app.ts
jose-carlos-sousa Jan 19, 2025
f8d5c94
fix: added image to product
jose-carlos-sousa Jan 20, 2025
ca8d46f
feat: dummy state of order
jose-carlos-sousa Jan 20, 2025
7503d26
fix: uncommented shield middleware
jose-carlos-sousa Jan 20, 2025
2f4076c
fix: reverted env.ts
jose-carlos-sousa Jan 20, 2025
77a18b1
Merge remote-tracking branch 'origin/develop' into 1-buy-early-bird-t…
jose-carlos-sousa Jan 26, 2025
aa3866b
fix: fixed package json
jose-carlos-sousa Jan 28, 2025
810c58e
ui: updated the pay button to use the MBWay logo
HenriqueSFernandes Jan 28, 2025
fef1e41
ui: removed separator
HenriqueSFernandes Jan 28, 2025
c406269
Merge branch 'ui/pay-with-mbway-button' into 1-buy-early-bird-ticket-…
HenriqueSFernandes Jan 28, 2025
3a2b3f5
fix: linter issues
HenriqueSFernandes Jan 28, 2025
3df94ef
added env variables to .env.example
HenriqueSFernandes Jan 28, 2025
29e7a32
feat: mensagens de erro iniciais
jose-carlos-sousa Jan 29, 2025
26b935f
feat: added loading indicator when a payment is being processed
HenriqueSFernandes Jan 29, 2025
e72ed5f
feat: added order confirmation popup
HenriqueSFernandes Jan 29, 2025
b18a874
feat: send users back to ticket page after payment
HenriqueSFernandes Jan 29, 2025
c78b471
feat: payment now use real data instead of hardcoded data
HenriqueSFernandes Jan 29, 2025
9a89a49
feat : added verification to show payment page
jose-carlos-sousa Jan 31, 2025
0eed5bc
feat: added group restrictions
jose-carlos-sousa Feb 1, 2025
80997cf
Merge branch 'develop' into 1-buy-early-bird-ticket-flow
jose-carlos-sousa Feb 1, 2025
ec27e1f
fix: removed useless files and fixed issues
jose-carlos-sousa Feb 4, 2025
7e46ac7
Merge remote-tracking branch 'origin/1-buy-early-bird-ticket-flow' in…
jose-carlos-sousa Feb 4, 2025
6a13f39
fix: fixed stock
jose-carlos-sousa Feb 4, 2025
85e3da1
fix:uncommented shield
jose-carlos-sousa Feb 4, 2025
b92f5c7
fix: removed useless console log
jose-carlos-sousa Feb 4, 2025
837030a
fix: added user info
jose-carlos-sousa Feb 4, 2025
efa156b
fix: build logo url with staticUrl()
tomaspalma Feb 5, 2025
baa130f
fix: title of tickets page with first uppercase letter
tomaspalma Feb 5, 2025
e2e5ed6
fix: remove class dark from inertia layout edge messing up the site
tomaspalma Feb 5, 2025
4ed0c07
fix: remove inertia public app url from coolify
tomaspalma Feb 5, 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
15 changes: 14 additions & 1 deletion docker-compose.coolify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,24 @@ services:
- "LOG_LEVEL=${LOG_LEVEL:-info}"
- "APP_KEY=${APP_KEY}"
- "SESSION_DRIVER=${SESSION_DRIVER:-cookie}"
- "REDIS_HOST=${REDIS_HOST:-valkey}"
- "REDIS_PORT=${REDIS_PORT:-6379}"
- "REDIS_PASSWORD=${REDIS_PASSWORD}"
- "FROM_EMAIL=${FROM_EMAIL:[email protected]}"
- "SMTP_HOST=${SMTP_HOST}"
- "SMTP_PORT=${SMTP_PORT}"
- "INERTIA_PUBLIC_TZ=${INERTIA_PUBLIC_TZ:-Europe/Lisbon}"
- "INERTIA_PUBLIC_EVENT_COUNTDOWN_DATE=${INERTIA_PUBLIC_EVENT_COUNTDOWN_DATE:-2025-04-11}"
- "INERTIA_PUBLIC_APP_URL=${INERTIA_PUBLIC_APP_URL}"

valkey:
image: valkey/valkey:8-alpine
command: ["valkey-server", "--save", "60", "1", "--loglevel", "warning"]
volumes:
- valkey-data:/data
environment:
- "VALKEY_EXTRA_FLAGS=${VALKEY_EXTRA_FLAGS}"

volumes:
website-tmp:
website-tmp:
valkey-data:
13 changes: 12 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,15 @@ services:
container_name: enei-mailpit
ports:
- "1025:1025"
- "8025:8025"
- "8025:8025"

valkey:
image: valkey/valkey:8-alpine
container_name: enei-valkey
volumes:
- valkey-data:/data
ports:
- "6379:6379"

volumes:
valkey-data:
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions website/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@ NODE_ENV=development
# Public facing app environment variables
APP_KEY=

# Payments
IFTHENPAY_MBWAY_KEY=

# Session
SESSION_DRIVER=cookie

# Jobs
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# E-mail
[email protected]
[email protected]
Expand Down
2 changes: 2 additions & 0 deletions website/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ yarn-error.log

# Platform specific
.DS_Store

dump.rdb
2 changes: 2 additions & 0 deletions website/adonisrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default defineConfig({
() => import('@adonisjs/lucid/commands'),
() => import('@adonisjs/mail/commands'),
() => import('@tuyau/core/commands'),
() => import('adonisjs-jobs/commands'),
],

/*
Expand Down Expand Up @@ -43,6 +44,7 @@ export default defineConfig({
() => import('@adonisjs/lucid/database_provider'),
() => import('@adonisjs/auth/auth_provider'),
() => import('@adonisjs/inertia/inertia_provider'),
() => import('adonisjs-jobs/jobs_provider'),
() => import('@adonisjs/mail/mail_provider'),
() => import('@tuyau/core/tuyau_provider'),
() => import('@adonisjs/ally/ally_provider'),
Expand Down
181 changes: 181 additions & 0 deletions website/app/controllers/orders_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import type { HttpContext } from '@adonisjs/core/http'
import env from '#start/env'
import axios from 'axios'
import Order from '#models/order'
import User from '#models/user'
import OrderProduct from '#models/order_product'
import Product from '#models/product'
import ProductGroup from '#models/product_group'
import { createMBWayOrderValidator } from '#validators/order'
import UpdateOrderStatus from '../jobs/update_order_status.js'
export default class OrdersController {
index({ inertia }: HttpContext) {
return inertia.render('payments/index')
}

public async createMBWay({ request, auth, response }: HttpContext) {
const authUser = auth.user

try {
// Validate input format
await request.validateUsing(createMBWayOrderValidator)

const { userId, products, nif, address, mobileNumber } = request.all()
limwa marked this conversation as resolved.
Show resolved Hide resolved
console.log(userId, products, nif, address, mobileNumber)

// Validate authentication

if (!authUser || authUser.id !== userId) {
return response.status(401).json({ message: 'Não autorizado' })
}

// Validate user existence

const user = await User.find(userId)

if (!user) {
return response.status(404).json({ message: 'Utilizador não encontrado' })
}

let totalAmount = 0
let description = ''

const productDetails = []

for (const productItem of products) {
const { productId, quantity } = productItem
const product = await Product.find(productId)

if (!product) {
return response
.status(404)
.json({ message: `Produto com id ${productId} não foi encontrado` })
}

const successfulOrdersOfGivenProduct = await OrderProduct.query()
.join('orders', 'order_products.order_id', 'orders.id')
.where('orders.user_id', userId)
.where('order_products.product_id', productId)
.where('orders.status', 'Success')



const totalQuantity = successfulOrdersOfGivenProduct.reduce(
(acc, orderProduct) => acc + orderProduct.quantity,
0
)

if (product.stock < quantity) {
return response
.status(400)
.json({ message: `Não há mais stock do produto ${product.name}` })
}

if (quantity + totalQuantity > product.max_order) {
return response.status(400).json({
message: `Apenas podes comprar ${product.max_order} do produto ${product.name}`,
})
}

const productGroup = await ProductGroup.find(product.productGroupId)
if(productGroup){

const sucessfulOrdersOfGivenGroup = await OrderProduct.query()
.join('orders', 'order_products.order_id', 'orders.id')
.join('products', 'order_products.product_id', 'products.id')
.where('orders.user_id', userId)
.where('products.product_group_id', product.productGroupId)
.where('orders.status', 'Success')



const totalGroupQuantity = sucessfulOrdersOfGivenGroup.reduce(
(acc, orderProduct) => acc + orderProduct.quantity,
0
)

if (totalGroupQuantity + quantity > productGroup.maxAmountPerGroup) {
return response.status(400).json({
message: `Apenas podes comprar ${productGroup?.maxAmountPerGroup} produtos do grupo ${productGroup.name}`,
})

}
}
productDetails.push({ product, quantity })
totalAmount += product.price * quantity
description += `${product.name} x${quantity}, `
}

description = `Payment for order: ${description.slice(0, -2)}`

// Create the order and associated products
const order = await Order.create({ userId, nif, address })

for (const { product, quantity } of productDetails) {
await OrderProduct.create({
orderId: order.id,
productId: product.id,
quantity,
})
}

// Prepare payment data

const data = {
mbWayKey: env.get('IFTHENPAY_MBWAY_KEY'),
orderId: order.id,
amount: totalAmount.toFixed(2),
mobileNumber,
description,
}

// Call payment API

const apiResponse = await axios.post('https://api.ifthenpay.com/spg/payment/mbway', data)

if (apiResponse.status === 200) {
const responseData = apiResponse.data
order.requestId = responseData.RequestId
order.status = 'Pending'
order.total = totalAmount
await order.save()

// Dispatch background job to update order status

await UpdateOrderStatus.dispatch(
{ requestId: order.requestId, email: authUser.email },
{ delay: 10000 }
).catch((error) => {
console.error('Error dispatching job', error)
})

return response.status(200).json({
order,
message: 'Payment initiated successfully',
})
} else {
return response.status(500).json({ message: 'Failed to initiate payment' })
}
} catch (error) {
console.error(error)
return response.status(500).json({
message: 'An error occurred while initiating the payment',
})
}
}

public async show({ inertia, params, auth, response }: HttpContext) {
const authUser = auth.user
if (!authUser) {
return response.status(401).json({
message: 'Unauthorized',
})
}

const order = await Order.find(params.id)
if (!order || (order.userId !== authUser.id)) {
return response.notFound({ message: 'Order not found' })
}
return inertia.render('payments/show', { order })
}
}
11 changes: 8 additions & 3 deletions website/app/controllers/tickets_controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import Ticket from '#models/ticket'
import Product from '#models/product'
import type { HttpContext } from '@adonisjs/core/http'

export default class TicketsController {
async index({ inertia }: HttpContext) {
const ticketTypes = await Ticket.all()
const ticketTypes = await Product.all()

return inertia.render('tickets', { ticketTypes })
}

async showPayment({ inertia, auth, params }: HttpContext) {
const ticket = await Product.find(params.id)

return inertia.render('payments/index', { ticket, user: auth.user })
}
}
74 changes: 74 additions & 0 deletions website/app/jobs/update_order_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import axios from 'axios'
import Order from '#models/order'
import env from '#start/env'
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'

type UpdateOrderStatusPayload = {
requestId: string
email: string
}

export default class UpdateOrderStatus extends Job {
async handle({ requestId, email }: UpdateOrderStatusPayload) {
try {

this.logger.info(`Processing status update for requestId: ${requestId}`)

// Fetch the order based on the requestId
const order = await Order.query().where('request_id', requestId).first()
if (!order) {
this.logger.error(`Order with requestId ${requestId} not found`)
console.error(`Order with requestId ${requestId} not found`)
return
}

if (order.status !== 'Pending') {
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
if (status) {
if (status === 'Pending') {
await UpdateOrderStatus.dispatch({ requestId, email }, { delay: 10000 }) // Retry after 5 seconds
this.logger.info(`Requeued job for requestId: ${requestId}`)
return
}
order.status = status
await order.save()
this.logger.info(`Order status updated to: ${order.status}`)
if (order.status === 'Success') {
this.logger.info(`Gonna send mail: ${order.status}`)
const products = await db
.from('products')
.join('order_products', 'products.id', 'order_products.product_id')
.where('order_products.order_id', order.id)
.select('products.*', 'order_products.quantity as quantity')

const total = order.total
const orderId = order.id
await mail.send(new ConfirmPaymentNotification(email, products, total, orderId))
}
} else {
await UpdateOrderStatus.dispatch({ requestId, email }, { delay: 10000 }) // Retry after 5 seconds
}
} else {
this.logger.error(`Failed to fetch payment status for requestId: ${requestId}`)
console.error(`Failed to fetch payment status for requestId: ${requestId}`)
await UpdateOrderStatus.dispatch({ requestId, email }, { delay: 10000 }) // Retry after 5 seconds
}
} catch (error) {
this.logger.error(`Error updating order status: ${error.message}`)
console.error(`Error updating order status: ${error.message}`)

await UpdateOrderStatus.dispatch({ requestId, email }, { delay: 10000 })
}
}
}
Loading