|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +/** |
| 4 | + * SEP-12 Customer Identification Routes |
| 5 | + * |
| 6 | + * GET /sep12/customer – retrieve KYC status (with requirement masking) |
| 7 | + * PUT /sep12/customer – submit / update PII fields |
| 8 | + * PUT /sep12/customer/requirements – admin: set merchant KYC requirements |
| 9 | + * |
| 10 | + * All endpoints require a valid JWT (authenticateToken). |
| 11 | + * The authenticated wallet address is used as the Stellar account unless an |
| 12 | + * admin explicitly passes ?account= (future extension). |
| 13 | + */ |
| 14 | + |
| 15 | +const express = require('express'); |
| 16 | +const router = express.Router(); |
| 17 | +const { authenticateToken } = require('../middleware/auth'); |
| 18 | +const { CustomerService } = require('../src/services/customerService'); |
| 19 | +const logger = require('../src/utils/logger'); |
| 20 | + |
| 21 | +// Stellar public key regex (G + 55 uppercase alphanumeric chars) |
| 22 | +const STELLAR_ACCOUNT_RE = /^G[A-Z0-9]{55}$/; |
| 23 | + |
| 24 | +function getService(req) { |
| 25 | + const db = req.app.get('database'); |
| 26 | + return new CustomerService(db); |
| 27 | +} |
| 28 | + |
| 29 | +// ─── GET /sep12/customer ──────────────────────────────────────────────────── |
| 30 | + |
| 31 | +/** |
| 32 | + * @query account Stellar public key (defaults to authenticated user) |
| 33 | + * @query merchant_id Optional – enables requirement masking |
| 34 | + * @query tier Optional – tier name for requirement masking |
| 35 | + */ |
| 36 | +router.get('/customer', authenticateToken, async (req, res) => { |
| 37 | + try { |
| 38 | + const account = req.query.account || req.user?.id; |
| 39 | + if (!account || !STELLAR_ACCOUNT_RE.test(account)) { |
| 40 | + return res.status(400).json({ error: 'Invalid or missing Stellar account' }); |
| 41 | + } |
| 42 | + |
| 43 | + const { merchant_id, tier } = req.query; |
| 44 | + const svc = getService(req); |
| 45 | + const result = await svc.getCustomer(account, merchant_id || null, tier || null); |
| 46 | + |
| 47 | + return res.json(result); |
| 48 | + } catch (err) { |
| 49 | + logger.error('[SEP-12] GET /customer error', { error: err.message }); |
| 50 | + return res.status(500).json({ error: 'Internal server error' }); |
| 51 | + } |
| 52 | +}); |
| 53 | + |
| 54 | +// ─── PUT /sep12/customer ──────────────────────────────────────────────────── |
| 55 | + |
| 56 | +/** |
| 57 | + * @body account Stellar public key (defaults to authenticated user) |
| 58 | + * @body full_name string |
| 59 | + * @body address string |
| 60 | + * @body date_of_birth string YYYY-MM-DD |
| 61 | + * @body id_photo_cid string IPFS CID |
| 62 | + */ |
| 63 | +router.put('/customer', authenticateToken, async (req, res) => { |
| 64 | + try { |
| 65 | + const account = req.body.account || req.user?.id; |
| 66 | + if (!account || !STELLAR_ACCOUNT_RE.test(account)) { |
| 67 | + return res.status(400).json({ error: 'Invalid or missing Stellar account' }); |
| 68 | + } |
| 69 | + |
| 70 | + const { full_name, address, date_of_birth, id_photo_cid } = req.body; |
| 71 | + const fields = {}; |
| 72 | + if (full_name !== undefined) fields.full_name = full_name; |
| 73 | + if (address !== undefined) fields.address = address; |
| 74 | + if (date_of_birth !== undefined) fields.date_of_birth = date_of_birth; |
| 75 | + if (id_photo_cid !== undefined) fields.id_photo_cid = id_photo_cid; |
| 76 | + |
| 77 | + if (Object.keys(fields).length === 0) { |
| 78 | + return res.status(400).json({ error: 'No KYC fields provided' }); |
| 79 | + } |
| 80 | + |
| 81 | + const svc = getService(req); |
| 82 | + const result = await svc.putCustomer(account, fields); |
| 83 | + |
| 84 | + return res.status(202).json(result); |
| 85 | + } catch (err) { |
| 86 | + logger.error('[SEP-12] PUT /customer error', { error: err.message }); |
| 87 | + return res.status(500).json({ error: 'Internal server error' }); |
| 88 | + } |
| 89 | +}); |
| 90 | + |
| 91 | +// ─── PUT /sep12/customer/requirements ────────────────────────────────────── |
| 92 | + |
| 93 | +/** |
| 94 | + * Admin endpoint: define which fields a merchant requires for a given tier. |
| 95 | + * |
| 96 | + * @body merchant_id |
| 97 | + * @body tier_name |
| 98 | + * @body requires_full_name boolean |
| 99 | + * @body requires_address boolean |
| 100 | + * @body requires_date_of_birth boolean |
| 101 | + * @body requires_id_photo boolean |
| 102 | + */ |
| 103 | +router.put('/customer/requirements', authenticateToken, async (req, res) => { |
| 104 | + try { |
| 105 | + const { merchant_id, tier_name, ...rest } = req.body; |
| 106 | + if (!merchant_id || !tier_name) { |
| 107 | + return res.status(400).json({ error: 'merchant_id and tier_name are required' }); |
| 108 | + } |
| 109 | + |
| 110 | + const allowed = ['requires_full_name', 'requires_address', 'requires_date_of_birth', 'requires_id_photo']; |
| 111 | + const requirements = {}; |
| 112 | + for (const key of allowed) { |
| 113 | + if (rest[key] !== undefined) requirements[key] = Boolean(rest[key]); |
| 114 | + } |
| 115 | + |
| 116 | + const svc = getService(req); |
| 117 | + await svc.setMerchantRequirements(merchant_id, tier_name, requirements); |
| 118 | + |
| 119 | + return res.json({ success: true, merchant_id, tier_name, requirements }); |
| 120 | + } catch (err) { |
| 121 | + logger.error('[SEP-12] PUT /customer/requirements error', { error: err.message }); |
| 122 | + return res.status(500).json({ error: 'Internal server error' }); |
| 123 | + } |
| 124 | +}); |
| 125 | + |
| 126 | +module.exports = router; |
0 commit comments