feat(rcart): add subscription/inventory/method-toggles to payment requests + fix 403

- Fix: add publicWrite to cartModule so payment routes aren't blocked
  by space-level write auth (routes have their own auth checks)
- Add paymentType field: 'single' (one-time) or 'subscription' (reusable QR)
- Add maxPayments inventory limit: QR codes auto-fill when limit reached
- Add paymentCount to track how many payments received
- Add enabledMethods: toggle card/wallet/encryptid per payment request
- Payment page only shows enabled tabs, shows inventory progress bar
- Subscriptions reset to 'pending' after each payment until filled
- New 'filled' status for inventory-limited payments that are full

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-11 16:35:11 -07:00
parent b8788fa81e
commit 636fc133fe
4 changed files with 222 additions and 9 deletions

View File

@ -379,7 +379,8 @@ class FolkPaymentPage extends HTMLElement {
const isPaid = p.status === 'paid' || p.status === 'confirmed'; const isPaid = p.status === 'paid' || p.status === 'confirmed';
const isExpired = p.status === 'expired'; const isExpired = p.status === 'expired';
const isCancelled = p.status === 'cancelled'; const isCancelled = p.status === 'cancelled';
const isTerminal = isPaid || isExpired || isCancelled; const isFilled = p.status === 'filled';
const isTerminal = isPaid || isExpired || isCancelled || isFilled;
const chainNames: Record<number, string> = { 8453: 'Base', 84532: 'Base Sepolia', 1: 'Ethereum' }; const chainNames: Record<number, string> = { 8453: 'Base', 84532: 'Base Sepolia', 1: 'Ethereum' };
const chainName = chainNames[p.chainId] || `Chain ${p.chainId}`; const chainName = chainNames[p.chainId] || `Chain ${p.chainId}`;
@ -411,7 +412,14 @@ class FolkPaymentPage extends HTMLElement {
<div class="description">${this.esc(p.description)}</div> <div class="description">${this.esc(p.description)}</div>
${p.paymentType === 'subscription' ? '<div class="type-badge">Subscription</div>' : ''}
${p.maxPayments > 0 ? `<div class="inventory-bar">
<div class="inventory-label">${p.paymentCount || 0} / ${p.maxPayments} payments</div>
<div class="inventory-track"><div class="inventory-fill" style="width:${Math.min(100, ((p.paymentCount || 0) / p.maxPayments) * 100)}%"></div></div>
</div>` : ''}
${isPaid ? this.renderPaidConfirmation() : ${isPaid ? this.renderPaidConfirmation() :
isFilled ? '<div class="terminal-msg">This payment request has reached its limit.</div>' :
isTerminal ? `<div class="terminal-msg">${isExpired ? 'This payment request has expired.' : 'This payment request has been cancelled.'}</div>` : isTerminal ? `<div class="terminal-msg">${isExpired ? 'This payment request has expired.' : 'This payment request has been cancelled.'}</div>` :
this.renderPaymentTabs()} this.renderPaymentTabs()}
@ -449,11 +457,26 @@ class FolkPaymentPage extends HTMLElement {
} }
private renderPaymentTabs(): string { private renderPaymentTabs(): string {
const methods = this.payment.enabledMethods || { card: true, wallet: true, encryptid: true };
const enabledTabs: Array<{ id: string; label: string }> = [];
if (methods.card) enabledTabs.push({ id: 'card', label: 'Card' });
if (methods.wallet) enabledTabs.push({ id: 'wallet', label: 'Wallet' });
if (methods.encryptid) enabledTabs.push({ id: 'encryptid', label: 'EncryptID' });
if (enabledTabs.length === 0) {
return '<div class="terminal-msg">No payment methods are available for this request.</div>';
}
// Auto-select first enabled tab if current tab is disabled
if (!enabledTabs.find(t => t.id === this.activeTab)) {
this.activeTab = enabledTabs[0].id as any;
}
return ` return `
<div class="tabs"> <div class="tabs">
<button class="tab ${this.activeTab === 'card' ? 'active' : ''}" data-tab="card">Card</button> ${enabledTabs.map(t =>
<button class="tab ${this.activeTab === 'wallet' ? 'active' : ''}" data-tab="wallet">Wallet</button> `<button class="tab ${this.activeTab === t.id ? 'active' : ''}" data-tab="${t.id}">${t.label}</button>`
<button class="tab ${this.activeTab === 'encryptid' ? 'active' : ''}" data-tab="encryptid">EncryptID</button> ).join('')}
</div> </div>
<div class="tab-content"> <div class="tab-content">
@ -595,6 +618,15 @@ class FolkPaymentPage extends HTMLElement {
.status-paid, .status-confirmed { background: rgba(34,197,94,0.15); color: #4ade80; } .status-paid, .status-confirmed { background: rgba(34,197,94,0.15); color: #4ade80; }
.status-expired { background: rgba(239,68,68,0.15); color: #f87171; } .status-expired { background: rgba(239,68,68,0.15); color: #f87171; }
.status-cancelled { background: rgba(156,163,175,0.15); color: #9ca3af; } .status-cancelled { background: rgba(156,163,175,0.15); color: #9ca3af; }
.status-filled { background: rgba(99,102,241,0.15); color: #818cf8; }
.type-badge { text-align: center; margin-bottom: 0.75rem; }
.type-badge { display: inline-block; padding: 0.1875rem 0.625rem; border-radius: 999px; font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; background: rgba(99,102,241,0.12); color: #818cf8; }
.inventory-bar { margin-bottom: 1rem; padding: 0.5rem 0.75rem; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 8px; }
.inventory-label { font-size: 0.75rem; color: var(--rs-text-secondary); margin-bottom: 0.375rem; text-align: center; }
.inventory-track { height: 6px; border-radius: 3px; background: rgba(255,255,255,0.08); overflow: hidden; }
.inventory-fill { height: 100%; border-radius: 3px; background: var(--rs-primary-hover); transition: width 0.3s ease; }
.amount-display { text-align: center; margin-bottom: 1rem; } .amount-display { text-align: center; margin-bottom: 1rem; }
.amount { display: block; font-size: 2rem; font-weight: 700; color: var(--rs-text-primary); } .amount { display: block; font-size: 2rem; font-weight: 700; color: var(--rs-text-primary); }

View File

@ -25,6 +25,9 @@ class FolkPaymentRequest extends HTMLElement {
private amountEditable = false; private amountEditable = false;
private token: 'USDC' | 'ETH' = 'USDC'; private token: 'USDC' | 'ETH' = 'USDC';
private chainId = 8453; private chainId = 8453;
private paymentType: 'single' | 'subscription' = 'single';
private maxPayments = 0; // 0 = unlimited
private enabledMethods = { card: true, wallet: true, encryptid: true };
// Result state // Result state
private generating = false; private generating = false;
@ -158,6 +161,9 @@ class FolkPaymentRequest extends HTMLElement {
token: this.token, token: this.token,
chainId: this.chainId, chainId: this.chainId,
recipientAddress: this.walletAddress, recipientAddress: this.walletAddress,
paymentType: this.paymentType,
maxPayments: this.maxPayments,
enabledMethods: this.enabledMethods,
}), }),
}); });
@ -197,6 +203,9 @@ class FolkPaymentRequest extends HTMLElement {
this.description = ''; this.description = '';
this.amount = ''; this.amount = '';
this.amountEditable = false; this.amountEditable = false;
this.paymentType = 'single';
this.maxPayments = 0;
this.enabledMethods = { card: true, wallet: true, encryptid: true };
this.render(); this.render();
} }
@ -273,7 +282,60 @@ class FolkPaymentRequest extends HTMLElement {
<label for="amount-editable">Let payer choose the amount</label> <label for="amount-editable">Let payer choose the amount</label>
</div> </div>
<button class="btn btn-primary btn-lg" data-action="generate" ${this.generating ? 'disabled' : ''}> <div class="section-divider"></div>
<div class="field">
<label class="label">Payment Type</label>
<div class="toggle-group">
<button class="toggle-btn ${this.paymentType === 'single' ? 'active' : ''}" data-payment-type="single">Single Payment</button>
<button class="toggle-btn ${this.paymentType === 'subscription' ? 'active' : ''}" data-payment-type="subscription">Subscription</button>
</div>
<span class="field-hint">${this.paymentType === 'subscription'
? 'QR stays active — accepts multiple payments over time'
: 'One-time payment — QR deactivates after payment'}</span>
</div>
<div class="field">
<label class="label">Inventory Limit</label>
<div class="field-row" style="align-items:center">
<input type="number" class="input" data-field="maxPayments" placeholder="Unlimited" min="0" step="1"
value="${this.maxPayments || ''}" style="width:120px" />
<span class="field-hint" style="margin:0">${this.maxPayments > 0
? `QR stops accepting after ${this.maxPayments} payment${this.maxPayments > 1 ? 's' : ''}`
: 'No limit — accepts payments indefinitely'}</span>
</div>
</div>
<div class="section-divider"></div>
<div class="field">
<label class="label">Accepted Payment Methods</label>
<div class="method-toggles">
<label class="method-toggle">
<input type="checkbox" data-method="card" ${this.enabledMethods.card ? 'checked' : ''} />
<span class="method-icon">&#128179;</span>
<span class="method-name">Credit Card</span>
<span class="method-desc">Transak on-ramp</span>
</label>
<label class="method-toggle">
<input type="checkbox" data-method="wallet" ${this.enabledMethods.wallet ? 'checked' : ''} />
<span class="method-icon">&#129523;</span>
<span class="method-name">External Wallet</span>
<span class="method-desc">MetaMask, etc.</span>
</label>
<label class="method-toggle">
<input type="checkbox" data-method="encryptid" ${this.enabledMethods.encryptid ? 'checked' : ''} />
<span class="method-icon">&#128273;</span>
<span class="method-name">EncryptID</span>
<span class="method-desc">Passkey wallet</span>
</label>
</div>
${!this.enabledMethods.card && !this.enabledMethods.wallet && !this.enabledMethods.encryptid
? '<div class="field-error">At least one payment method must be enabled</div>' : ''}
</div>
<button class="btn btn-primary btn-lg" data-action="generate"
${this.generating || (!this.enabledMethods.card && !this.enabledMethods.wallet && !this.enabledMethods.encryptid) ? 'disabled' : ''}>
${this.generating ? 'Generating...' : 'Generate QR Code'} ${this.generating ? 'Generating...' : 'Generate QR Code'}
</button> </button>
${this.authError ? `<div class="field-error">${this.esc(this.authError)}</div>` : ''} ${this.authError ? `<div class="field-error">${this.esc(this.authError)}</div>` : ''}
@ -286,12 +348,23 @@ class FolkPaymentRequest extends HTMLElement {
? 'Any amount' ? 'Any amount'
: `${p.amount} ${p.token}`; : `${p.amount} ${p.token}`;
const tags: string[] = [];
if (this.paymentType === 'subscription') tags.push('Subscription');
if (this.amountEditable) tags.push('Editable amount');
if (this.maxPayments > 0) tags.push(`Limit: ${this.maxPayments}`);
const methods = [
this.enabledMethods.card ? 'Card' : '',
this.enabledMethods.wallet ? 'Wallet' : '',
this.enabledMethods.encryptid ? 'Passkey' : '',
].filter(Boolean);
return ` return `
<div class="result"> <div class="result">
<div class="result-header"> <div class="result-header">
<div class="result-desc">${this.esc(p.description)}</div> <div class="result-desc">${this.esc(p.description)}</div>
<div class="result-amount">${amountDisplay}</div> <div class="result-amount">${amountDisplay}</div>
${this.amountEditable ? '<div class="result-hint">Amount editable by payer</div>' : ''} ${tags.length ? `<div class="result-tags">${tags.map(t => `<span class="result-tag">${t}</span>`).join('')}</div>` : ''}
<div class="result-hint">Accepts: ${methods.join(', ')}</div>
</div> </div>
<div class="qr-display"> <div class="qr-display">
@ -340,6 +413,30 @@ class FolkPaymentRequest extends HTMLElement {
this.render(); this.render();
}); });
// Payment type toggle
this.shadow.querySelectorAll('[data-payment-type]').forEach((el) => {
el.addEventListener('click', () => {
this.paymentType = (el as HTMLElement).dataset.paymentType as 'single' | 'subscription';
this.render();
});
});
// Max payments
const maxInput = this.shadow.querySelector('[data-field="maxPayments"]') as HTMLInputElement;
maxInput?.addEventListener('input', () => {
this.maxPayments = parseInt(maxInput.value) || 0;
this.render();
});
// Payment method toggles
this.shadow.querySelectorAll('[data-method]').forEach((el) => {
el.addEventListener('change', () => {
const method = (el as HTMLInputElement).dataset.method as keyof typeof this.enabledMethods;
this.enabledMethods = { ...this.enabledMethods, [method]: (el as HTMLInputElement).checked };
this.render();
});
});
// Generate // Generate
this.shadow.querySelector('[data-action="generate"]')?.addEventListener('click', () => this.generatePayment()); this.shadow.querySelector('[data-action="generate"]')?.addEventListener('click', () => this.generatePayment());
@ -422,12 +519,32 @@ class FolkPaymentRequest extends HTMLElement {
.btn-outline:hover { border-color: var(--rs-text-secondary); color: var(--rs-text-primary); } .btn-outline:hover { border-color: var(--rs-text-secondary); color: var(--rs-text-primary); }
.field-error { color: #f87171; font-size: 0.8125rem; margin-top: 0.5rem; } .field-error { color: #f87171; font-size: 0.8125rem; margin-top: 0.5rem; }
.field-hint { color: var(--rs-text-muted); font-size: 0.75rem; margin-top: 0.25rem; }
.section-divider { height: 1px; background: var(--rs-border); margin: 0.5rem 0; }
.toggle-group { display: flex; gap: 0; border: 1px solid var(--rs-border); border-radius: 8px; overflow: hidden; }
.toggle-btn { flex: 1; padding: 0.5rem 0.75rem; border: none; background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 0.8125rem; font-weight: 500; transition: all 0.15s; }
.toggle-btn:not(:last-child) { border-right: 1px solid var(--rs-border); }
.toggle-btn.active { background: var(--rs-primary-hover); color: #fff; }
.toggle-btn:hover:not(.active) { background: var(--rs-bg-hover); }
.method-toggles { display: flex; flex-direction: column; gap: 0.5rem; }
.method-toggle { display: flex; align-items: center; gap: 0.625rem; padding: 0.625rem 0.75rem; border: 1px solid var(--rs-border); border-radius: 8px; cursor: pointer; transition: border-color 0.15s; }
.method-toggle:hover { border-color: var(--rs-border-strong); }
.method-toggle:has(input:checked) { border-color: var(--rs-primary); background: rgba(99,102,241,0.05); }
.method-toggle input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--rs-primary-hover); cursor: pointer; flex-shrink: 0; }
.method-icon { font-size: 1.125rem; width: 24px; text-align: center; flex-shrink: 0; }
.method-name { color: var(--rs-text-primary); font-size: 0.875rem; font-weight: 500; }
.method-desc { color: var(--rs-text-muted); font-size: 0.75rem; margin-left: auto; }
/* Result */ /* Result */
.result { text-align: center; } .result { text-align: center; }
.result-header { margin-bottom: 1.5rem; } .result-header { margin-bottom: 1.5rem; }
.result-desc { color: var(--rs-text-secondary); font-size: 1rem; margin-bottom: 0.25rem; } .result-desc { color: var(--rs-text-secondary); font-size: 1rem; margin-bottom: 0.25rem; }
.result-amount { color: var(--rs-text-primary); font-size: 2rem; font-weight: 700; } .result-amount { color: var(--rs-text-primary); font-size: 2rem; font-weight: 700; }
.result-tags { display: flex; gap: 0.375rem; justify-content: center; flex-wrap: wrap; margin-top: 0.5rem; }
.result-tag { padding: 0.1875rem 0.5rem; border-radius: 999px; font-size: 0.6875rem; font-weight: 600; background: rgba(99,102,241,0.12); color: var(--rs-primary-hover); text-transform: uppercase; letter-spacing: 0.03em; }
.result-hint { color: var(--rs-text-muted); font-size: 0.8125rem; margin-top: 0.25rem; } .result-hint { color: var(--rs-text-muted); font-size: 0.8125rem; margin-top: 0.25rem; }
.qr-display { margin: 0 auto 1.5rem; padding: 1rem; background: #fff; border-radius: 12px; display: inline-block; } .qr-display { margin: 0 auto 1.5rem; padding: 1rem; background: #fff; border-radius: 12px; display: inline-block; }

View File

@ -1138,6 +1138,9 @@ routes.post("/api/payments", async (c) => {
chainId = 8453, recipientAddress, chainId = 8453, recipientAddress,
fiatAmount = null, fiatCurrency = 'USD', fiatAmount = null, fiatCurrency = 'USD',
expiresIn = 0, // seconds, 0 = no expiry expiresIn = 0, // seconds, 0 = no expiry
paymentType = 'single',
maxPayments = 0,
enabledMethods = { card: true, wallet: true, encryptid: true },
} = body; } = body;
if (!description || !recipientAddress) { if (!description || !recipientAddress) {
@ -1167,6 +1170,14 @@ routes.post("/api/payments", async (c) => {
d.payment.fiatCurrency = fiatCurrency; d.payment.fiatCurrency = fiatCurrency;
d.payment.creatorDid = claims.sub; d.payment.creatorDid = claims.sub;
d.payment.status = 'pending'; d.payment.status = 'pending';
d.payment.paymentType = paymentType === 'subscription' ? 'subscription' : 'single';
d.payment.maxPayments = Math.max(0, parseInt(maxPayments) || 0);
d.payment.paymentCount = 0;
d.payment.enabledMethods = {
card: enabledMethods.card !== false,
wallet: enabledMethods.wallet !== false,
encryptid: enabledMethods.encryptid !== false,
};
d.payment.createdAt = now; d.payment.createdAt = now;
d.payment.updatedAt = now; d.payment.updatedAt = now;
d.payment.expiresAt = expiresAt; d.payment.expiresAt = expiresAt;
@ -1246,6 +1257,19 @@ routes.get("/api/payments/:id", async (c) => {
}); });
} }
// Check inventory fill
if (p.maxPayments > 0 && p.paymentCount >= p.maxPayments && p.status === 'pending') {
_syncServer!.changeDoc<PaymentRequestDoc>(docId, 'fill payment', (d) => {
d.payment.status = 'filled';
d.payment.updatedAt = Date.now();
});
return c.json({
...paymentToResponse(p),
status: 'filled',
usdcAddress: USDC_ADDRESSES[p.chainId] || null,
});
}
return c.json({ return c.json({
...paymentToResponse(p), ...paymentToResponse(p),
usdcAddress: USDC_ADDRESSES[p.chainId] || null, usdcAddress: USDC_ADDRESSES[p.chainId] || null,
@ -1262,11 +1286,17 @@ routes.patch("/api/payments/:id/status", async (c) => {
const body = await c.req.json(); const body = await c.req.json();
const { status, txHash, paymentMethod, payerIdentity, transakOrderId, amount } = body; const { status, txHash, paymentMethod, payerIdentity, transakOrderId, amount } = body;
const validStatuses = ['pending', 'paid', 'confirmed', 'expired', 'cancelled']; const validStatuses = ['pending', 'paid', 'confirmed', 'expired', 'cancelled', 'filled'];
if (status && !validStatuses.includes(status)) { if (status && !validStatuses.includes(status)) {
return c.json({ error: `status must be one of: ${validStatuses.join(", ")}` }, 400); return c.json({ error: `status must be one of: ${validStatuses.join(", ")}` }, 400);
} }
// Check if inventory is full before allowing payment
const p = doc.payment;
if (status === 'paid' && p.maxPayments > 0 && p.paymentCount >= p.maxPayments) {
return c.json({ error: "This payment request has reached its limit" }, 400);
}
const now = Date.now(); const now = Date.now();
_syncServer!.changeDoc<PaymentRequestDoc>(docId, `payment status → ${status || 'update'}`, (d) => { _syncServer!.changeDoc<PaymentRequestDoc>(docId, `payment status → ${status || 'update'}`, (d) => {
if (status) d.payment.status = status; if (status) d.payment.status = status;
@ -1279,7 +1309,19 @@ routes.patch("/api/payments/:id/status", async (c) => {
d.payment.amount = String(amount); d.payment.amount = String(amount);
} }
d.payment.updatedAt = now; d.payment.updatedAt = now;
if (status === 'paid') d.payment.paidAt = now; if (status === 'paid') {
d.payment.paidAt = now;
d.payment.paymentCount = (d.payment.paymentCount || 0) + 1;
// For subscriptions/multi-pay, reset to pending after payment (unless filled)
if (d.payment.paymentType === 'subscription' || d.payment.maxPayments > 1) {
if (d.payment.maxPayments > 0 && d.payment.paymentCount >= d.payment.maxPayments) {
d.payment.status = 'filled';
} else {
// Keep accepting payments — reset status after recording
d.payment.status = 'pending';
}
}
}
}); });
const updated = _syncServer!.getDoc<PaymentRequestDoc>(docId); const updated = _syncServer!.getDoc<PaymentRequestDoc>(docId);
@ -1311,6 +1353,7 @@ routes.post("/api/payments/:id/transak-session", async (c) => {
const p = doc.payment; const p = doc.payment;
if (p.status !== 'pending') return c.json({ error: "Payment is no longer pending" }, 400); if (p.status !== 'pending') return c.json({ error: "Payment is no longer pending" }, 400);
if (p.enabledMethods && !p.enabledMethods.card) return c.json({ error: "Card payments are not enabled for this request" }, 400);
const { email } = await c.req.json(); const { email } = await c.req.json();
if (!email) return c.json({ error: "Required: email" }, 400); if (!email) return c.json({ error: "Required: email" }, 400);
@ -1358,6 +1401,10 @@ function paymentToResponse(p: PaymentRequestMeta) {
txHash: p.txHash, txHash: p.txHash,
payerIdentity: p.payerIdentity, payerIdentity: p.payerIdentity,
transakOrderId: p.transakOrderId, transakOrderId: p.transakOrderId,
paymentType: p.paymentType || 'single',
maxPayments: p.maxPayments || 0,
paymentCount: p.paymentCount || 0,
enabledMethods: p.enabledMethods || { card: true, wallet: true, encryptid: true },
created_at: new Date(p.createdAt).toISOString(), created_at: new Date(p.createdAt).toISOString(),
updated_at: new Date(p.updatedAt).toISOString(), updated_at: new Date(p.updatedAt).toISOString(),
paid_at: p.paidAt ? new Date(p.paidAt).toISOString() : null, paid_at: p.paidAt ? new Date(p.paidAt).toISOString() : null,
@ -1461,6 +1508,7 @@ export const cartModule: RSpaceModule = {
name: "rCart", name: "rCart",
icon: "🛒", icon: "🛒",
description: "Group shopping & cosmolocal print-on-demand shop", description: "Group shopping & cosmolocal print-on-demand shop",
publicWrite: true,
scoping: { defaultScope: 'space', userConfigurable: false }, scoping: { defaultScope: 'space', userConfigurable: false },
docSchemas: [ docSchemas: [
{ pattern: '{space}:cart:catalog', description: 'Product catalog', init: catalogSchema.init }, { pattern: '{space}:cart:catalog', description: 'Product catalog', init: catalogSchema.init },

View File

@ -287,11 +287,23 @@ export interface PaymentRequestMeta {
fiatAmount: string | null; fiatAmount: string | null;
fiatCurrency: string; fiatCurrency: string;
creatorDid: string; creatorDid: string;
status: 'pending' | 'paid' | 'confirmed' | 'expired' | 'cancelled'; status: 'pending' | 'paid' | 'confirmed' | 'expired' | 'cancelled' | 'filled';
paymentMethod: 'transak' | 'wallet' | 'encryptid' | null; paymentMethod: 'transak' | 'wallet' | 'encryptid' | null;
txHash: string | null; txHash: string | null;
payerIdentity: string | null; payerIdentity: string | null;
transakOrderId: string | null; transakOrderId: string | null;
// Payment type: one-time or recurring subscription
paymentType: 'single' | 'subscription';
// Inventory: max number of payments (0 = unlimited)
maxPayments: number;
// How many have been paid
paymentCount: number;
// Which payment methods are enabled for payers
enabledMethods: {
card: boolean;
wallet: boolean;
encryptid: boolean;
};
createdAt: number; createdAt: number;
updatedAt: number; updatedAt: number;
paidAt: number; paidAt: number;
@ -337,6 +349,10 @@ export const paymentRequestSchema: DocSchema<PaymentRequestDoc> = {
txHash: null, txHash: null,
payerIdentity: null, payerIdentity: null,
transakOrderId: null, transakOrderId: null,
paymentType: 'single',
maxPayments: 0,
paymentCount: 0,
enabledMethods: { card: true, wallet: true, encryptid: true },
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
paidAt: 0, paidAt: 0,