226 lines
5.8 KiB
TypeScript
226 lines
5.8 KiB
TypeScript
import { Router, Response } from 'express';
|
|
import { store } from '../services/Store';
|
|
import { paymentProvider } from '../services/PaymentProvider';
|
|
import { keyActivationProvider } from '../services/KeyActivationProvider';
|
|
import { encryptionService } from '../utils/encryption';
|
|
import { mockAuthMiddleware, requireSteamAuth } from '../middleware/auth';
|
|
import { config } from '../config';
|
|
import { CreateTransactionDto } from '../models';
|
|
|
|
const router = Router();
|
|
|
|
router.post('/', mockAuthMiddleware, async (req, res: Response) => {
|
|
const { listingId } = req.body as CreateTransactionDto;
|
|
|
|
if (!listingId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'listingId is required',
|
|
});
|
|
}
|
|
|
|
const listing = store.getListingById(listingId);
|
|
|
|
if (!listing) {
|
|
return res.status(404).json({ success: false, error: 'Listing not found' });
|
|
}
|
|
|
|
if (listing.status !== 'ACTIVE') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Listing is not available',
|
|
});
|
|
}
|
|
|
|
if (listing.sellerId === req.userId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Cannot buy your own listing',
|
|
});
|
|
}
|
|
|
|
const holdResult = await paymentProvider.createHold(listing.price, listing.currency);
|
|
|
|
if (!holdResult.success) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: holdResult.error || 'Payment failed',
|
|
});
|
|
}
|
|
|
|
const transaction = store.createTransaction({
|
|
listingId: listing.id,
|
|
buyerId: req.userId!,
|
|
sellerId: listing.sellerId,
|
|
amount: listing.price,
|
|
currency: listing.currency,
|
|
escrowStatus: 'HELD',
|
|
transactionStatus: 'PENDING',
|
|
holdId: holdResult.holdId,
|
|
keyDelivered: false,
|
|
});
|
|
|
|
store.updateListing(listing.id, { status: 'SOLD' });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
transaction: {
|
|
id: transaction.id,
|
|
listingId: transaction.listingId,
|
|
amount: transaction.amount,
|
|
currency: transaction.currency,
|
|
escrowStatus: transaction.escrowStatus,
|
|
transactionStatus: transaction.transactionStatus,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
router.get('/:id', mockAuthMiddleware, (req, res: Response) => {
|
|
const transaction = store.getTransactionById(req.params.id);
|
|
|
|
if (!transaction) {
|
|
return res.status(404).json({ success: false, error: 'Transaction not found' });
|
|
}
|
|
|
|
if (transaction.buyerId !== req.userId && transaction.sellerId !== req.userId) {
|
|
return res.status(403).json({ success: false, error: 'Forbidden' });
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: { transaction },
|
|
});
|
|
});
|
|
|
|
router.get('/:id/key', mockAuthMiddleware, (req, res: Response) => {
|
|
const transaction = store.getTransactionById(req.params.id);
|
|
|
|
if (!transaction) {
|
|
return res.status(404).json({ success: false, error: 'Transaction not found' });
|
|
}
|
|
|
|
if (transaction.buyerId !== req.userId) {
|
|
return res.status(403).json({ success: false, error: 'Forbidden' });
|
|
}
|
|
|
|
if (transaction.escrowStatus !== 'HELD') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Key only available when payment is held in escrow',
|
|
});
|
|
}
|
|
|
|
if (transaction.keyDelivered) {
|
|
return res.json({
|
|
success: true,
|
|
data: {
|
|
keyAlreadyDelivered: true,
|
|
message: 'Key was already delivered in this transaction',
|
|
},
|
|
});
|
|
}
|
|
|
|
const listing = store.getListingById(transaction.listingId);
|
|
if (!listing) {
|
|
return res.status(404).json({ success: false, error: 'Listing not found' });
|
|
}
|
|
|
|
const key = encryptionService.decrypt(listing.keyEncrypted);
|
|
|
|
store.updateTransaction(transaction.id, { keyDelivered: true });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: { key },
|
|
});
|
|
});
|
|
|
|
router.post('/:id/confirm', requireSteamAuth, async (req, res: Response) => {
|
|
const { status } = req.body;
|
|
|
|
if (!status || !['SUCCESS', 'FAILED'].includes(status)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'status must be SUCCESS or FAILED',
|
|
});
|
|
}
|
|
|
|
const transaction = store.getTransactionById(req.params.id);
|
|
|
|
if (!transaction) {
|
|
return res.status(404).json({ success: false, error: 'Transaction not found' });
|
|
}
|
|
|
|
if (transaction.buyerId !== req.userId) {
|
|
return res.status(403).json({ success: false, error: 'Forbidden' });
|
|
}
|
|
|
|
if (transaction.transactionStatus !== 'PENDING') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Transaction already confirmed or disputed',
|
|
});
|
|
}
|
|
|
|
if (status === 'SUCCESS') {
|
|
if (transaction.holdId) {
|
|
await paymentProvider.release(transaction.holdId);
|
|
}
|
|
|
|
store.updateTransaction(transaction.id, {
|
|
escrowStatus: 'RELEASED',
|
|
transactionStatus: 'COMPLETED',
|
|
confirmedAt: new Date(),
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
message: 'Key confirmed. Payment released to seller.',
|
|
escrowStatus: 'RELEASED',
|
|
},
|
|
});
|
|
} else {
|
|
if (transaction.holdId) {
|
|
await paymentProvider.refund(transaction.holdId);
|
|
}
|
|
|
|
store.updateTransaction(transaction.id, {
|
|
escrowStatus: 'REFUNDED',
|
|
transactionStatus: 'DISPUTED',
|
|
confirmedAt: new Date(),
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
message: 'Key marked as failed. Payment refunded to buyer.',
|
|
escrowStatus: 'REFUNDED',
|
|
transactionStatus: 'DISPUTED',
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/buyer/me', mockAuthMiddleware, (req, res: Response) => {
|
|
const transactions = store.getTransactionsByBuyer(req.userId!);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: { transactions },
|
|
});
|
|
});
|
|
|
|
router.get('/seller/me', mockAuthMiddleware, (req, res: Response) => {
|
|
const transactions = store.getTransactionsBySeller(req.userId!);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: { transactions },
|
|
});
|
|
});
|
|
|
|
export default router;
|