@africode/core 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
- package/LICENSE +623 -0
- package/README.md +442 -0
- package/bin/africode.js +73 -0
- package/bin/africode.js.1758507140 +343 -0
- package/bin/cli.ts +83 -0
- package/bin/create-africode.js +158 -0
- package/bin/scaffold.ts +219 -0
- package/components/accordion.js +183 -0
- package/components/alert.js +131 -0
- package/components/auth.js +172 -0
- package/components/avatar.js +117 -0
- package/components/badge.js +104 -0
- package/components/base.d.ts +139 -0
- package/components/base.js +184 -0
- package/components/button.js +164 -0
- package/components/card.js +137 -0
- package/components/cultural-card.js +243 -0
- package/components/divider.js +83 -0
- package/components/dropdown.js +171 -0
- package/components/error-boundary.js +155 -0
- package/components/form.js +131 -0
- package/components/grid.js +273 -0
- package/components/hero.js +138 -0
- package/components/icon.js +36 -0
- package/components/index.js +57 -0
- package/components/input.js +256 -0
- package/components/kanga-card.js +185 -0
- package/components/language-switcher.js +108 -0
- package/components/loader.js +80 -0
- package/components/modal.js +262 -0
- package/components/motion.js +84 -0
- package/components/navbar.js +236 -0
- package/components/pattern-showcase.js +225 -0
- package/components/progress.js +134 -0
- package/components/react.js +111 -0
- package/components/section.js +54 -0
- package/components/select.js +322 -0
- package/components/sidebar.js +180 -0
- package/components/skeleton.js +85 -0
- package/components/table.js +181 -0
- package/components/tabs.js +202 -0
- package/components/theme-toggle.js +82 -0
- package/components/toast.js +139 -0
- package/components/tooltip.js +167 -0
- package/core/a2ui-schema-manager.js +344 -0
- package/core/a2ui.js +431 -0
- package/core/bun-runtime.js +799 -0
- package/core/cli/commands/add.js +23 -0
- package/core/cli/commands/audit.js +58 -0
- package/core/cli/commands/build.js +137 -0
- package/core/cli/commands/create-plugin.js +241 -0
- package/core/cli/commands/dev.js +228 -0
- package/core/cli/commands/lint.js +23 -0
- package/core/cli/commands/test.js +34 -0
- package/core/cli/migrator.js +71 -0
- package/core/cli/ui.js +46 -0
- package/core/compliance.js +628 -0
- package/core/config.js +263 -0
- package/core/db-advanced.js +481 -0
- package/core/db.js +284 -0
- package/core/enhanced-hmr.js +404 -0
- package/core/errors.js +222 -0
- package/core/file-router.js +290 -0
- package/core/heartbeat.js +64 -0
- package/core/hmr-client.js +204 -0
- package/core/hmr.js +196 -0
- package/core/html.d.ts +116 -0
- package/core/html.js +160 -0
- package/core/hydration.js +52 -0
- package/core/lipa-namba-journey.js +572 -0
- package/core/motion.js +106 -0
- package/core/nida-cig-middleware.js +455 -0
- package/core/patterns.d.ts +124 -0
- package/core/patterns.js +833 -0
- package/core/plugins/index.js +312 -0
- package/core/router.js +387 -0
- package/core/sdk-client.js +62 -0
- package/core/sdk.d.ts +133 -0
- package/core/sdk.js +123 -0
- package/core/seo.js +76 -0
- package/core/server/auth-endpoints.js +339 -0
- package/core/server/auth.js +180 -0
- package/core/server/csrf.js +206 -0
- package/core/server/db.js +39 -0
- package/core/server/middleware.js +324 -0
- package/core/server/rate-limit.js +238 -0
- package/core/server/render.js +69 -0
- package/core/server/router.js +120 -0
- package/core/shim.js +28 -0
- package/core/state.d.ts +86 -0
- package/core/state.js +242 -0
- package/core/store.d.ts +122 -0
- package/core/store.js +61 -0
- package/core/validation.d.ts +233 -0
- package/core/validation.js +590 -0
- package/core/websocket.js +639 -0
- package/dist/africode.js +2905 -0
- package/dist/africode.js.map +61 -0
- package/dist/build-info.json +23 -0
- package/dist/components.js +2888 -0
- package/dist/components.js.map +58 -0
- package/dist/styles/africanity.css +322 -0
- package/dist/styles/typography.css +141 -0
- package/docs/IDE-Guide.md +50 -0
- package/package.json +110 -0
- package/src/index.ts +196 -0
- package/styles/africanity.css +322 -0
- package/styles/typography.css +141 -0
- package/templates/starter/.env.example +15 -0
- package/templates/starter/africode.config.js +40 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/pages/index.html +46 -0
- package/templates/starter/src/pages/index.js +32 -0
- package/templates/starter/src/styles/main.css +4 -0
- package/templates/starter-3d/.env.example +7 -0
- package/templates/starter-3d/africode.config.js +29 -0
- package/templates/starter-3d/components/af-model-viewer.js +125 -0
- package/templates/starter-3d/package.json +15 -0
- package/templates/starter-3d/src/pages/index.html +46 -0
- package/templates/starter-3d/src/pages/index.js +50 -0
- package/templates/starter-3d/src/styles/main.css +4 -0
- package/templates/starter-react/.env.example +15 -0
- package/templates/starter-react/africode.config.js +40 -0
- package/templates/starter-react/package.json +16 -0
- package/templates/starter-react/src/pages/index.html +46 -0
- package/templates/starter-react/src/pages/index.js +68 -0
- package/templates/starter-react/src/styles/main.css +4 -0
- package/templates/starter-tailwind/.env.example +15 -0
- package/templates/starter-tailwind/africode.config.js +40 -0
- package/templates/starter-tailwind/package.json +20 -0
- package/templates/starter-tailwind/src/pages/index.html +46 -0
- package/templates/starter-tailwind/src/pages/index.js +37 -0
- package/templates/starter-tailwind/src/styles/main.css +4 -0
- package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
- package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Islands Architecture Hydration Engine
|
|
3
|
+
*
|
|
4
|
+
* Efficiently loads component JavaScript only when elements are visible.
|
|
5
|
+
* This enables the "0 KB JS" baseline for initial views.
|
|
6
|
+
*
|
|
7
|
+
* @module core/hydration
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hydrate components based on visibility
|
|
12
|
+
* @param {Object} componentMap - Map of tag names to dynamic import functions
|
|
13
|
+
*/
|
|
14
|
+
export function hydrate(componentMap) {
|
|
15
|
+
if (typeof window === 'undefined') {return;}
|
|
16
|
+
if (!('IntersectionObserver' in window)) {
|
|
17
|
+
// Fallback for ancient browsers: load everything immediately
|
|
18
|
+
Object.values(componentMap).forEach(load => load());
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const observer = new IntersectionObserver((entries) => {
|
|
23
|
+
entries.forEach(entry => {
|
|
24
|
+
if (entry.isIntersecting) {
|
|
25
|
+
const tag = entry.target.tagName.toLowerCase();
|
|
26
|
+
const load = componentMap[tag];
|
|
27
|
+
|
|
28
|
+
if (load) {
|
|
29
|
+
console.log(`🏝️ Hydrating Island: <${tag}>`);
|
|
30
|
+
load().then(() => {
|
|
31
|
+
// Optimally, we could stop observing *all* of this tag type since definition is global
|
|
32
|
+
// But for simplicity, we just unobserve this one.
|
|
33
|
+
// The custom element upgrade happens automatically.
|
|
34
|
+
|
|
35
|
+
// Remove from map to prevent double-loading attempts (though imports are cached)
|
|
36
|
+
delete componentMap[tag];
|
|
37
|
+
}).catch(err => console.error(`Failed to hydrate <${tag}>`, err));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Stop observing this element regardless
|
|
41
|
+
observer.unobserve(entry.target);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}, { rootMargin: '50px 0px' }); // Pre-load 50px before view
|
|
45
|
+
|
|
46
|
+
// Observe all registered tags locally
|
|
47
|
+
Object.keys(componentMap).forEach(tag => {
|
|
48
|
+
document.querySelectorAll(tag).forEach(el => observer.observe(el));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// TODO: Add MutationObserver for dynamically added nodes if needed
|
|
52
|
+
}
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lipa Namba Journey - Framework-Native Primitive
|
|
3
|
+
* Bank of Tanzania (BoT) Compliant Payment Flows
|
|
4
|
+
*
|
|
5
|
+
* Implements complete merchant payment lifecycle:
|
|
6
|
+
* - Lipa Namba (Pay by Phone Number)
|
|
7
|
+
* - Lipa kwa Simu (Pay by SIM)
|
|
8
|
+
* - Merchant KYC/AML compliance
|
|
9
|
+
* - Real-time transaction processing
|
|
10
|
+
* - Automated regulatory reporting
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
|
|
16
|
+
export class LipaNambaJourney extends EventEmitter {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
super();
|
|
19
|
+
|
|
20
|
+
this.config = {
|
|
21
|
+
tipsEndpoint: config.tipsEndpoint || 'https://api.tips.go.tz',
|
|
22
|
+
nidaMiddleware: config.nidaMiddleware,
|
|
23
|
+
amlEngine: config.amlEngine,
|
|
24
|
+
database: config.database,
|
|
25
|
+
redis: config.redis, // For session management
|
|
26
|
+
...config
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// BoT Regulatory Limits (as of 2026)
|
|
30
|
+
this.regulatoryLimits = {
|
|
31
|
+
merchant: {
|
|
32
|
+
daily: 100_000_000, // 100M TZS
|
|
33
|
+
perTransaction: 25_000_000, // 25M TZS
|
|
34
|
+
monthlyVolume: 1_000_000_000 // 1B TZS
|
|
35
|
+
},
|
|
36
|
+
customer: {
|
|
37
|
+
daily: 50_000_000, // 50M TZS
|
|
38
|
+
perTransaction: 10_000_000, // 10M TZS
|
|
39
|
+
monthlyVolume: 500_000_000 // 500M TZS
|
|
40
|
+
},
|
|
41
|
+
sessionTimeout: 30 * 60 * 1000, // 30 minutes
|
|
42
|
+
otpTimeout: 5 * 60 * 1000 // 5 minutes
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this._initializeEventHandlers();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initialize event handlers for journey lifecycle
|
|
50
|
+
*/
|
|
51
|
+
_initializeEventHandlers() {
|
|
52
|
+
this.on('payment:initiated', this._handlePaymentInitiated.bind(this));
|
|
53
|
+
this.on('customer:identified', this._handleCustomerIdentified.bind(this));
|
|
54
|
+
this.on('aml:screened', this._handleAMLScreened.bind(this));
|
|
55
|
+
this.on('payment:confirmed', this._handlePaymentConfirmed.bind(this));
|
|
56
|
+
this.on('payment:completed', this._handlePaymentCompleted.bind(this));
|
|
57
|
+
this.on('payment:failed', this._handlePaymentFailed.bind(this));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Execute complete Lipa Namba journey end-to-end
|
|
62
|
+
* Framework-native primitive for seamless integration
|
|
63
|
+
*/
|
|
64
|
+
async executeFullJourney(merchantRequest, customerIdentity, confirmationData) {
|
|
65
|
+
try {
|
|
66
|
+
this.emit('journey:started', { merchantRequest });
|
|
67
|
+
|
|
68
|
+
// Step 1: Merchant Payment Initiation
|
|
69
|
+
const initiation = await this.initiateMerchantPayment(merchantRequest);
|
|
70
|
+
if (!initiation.ok) {
|
|
71
|
+
throw new Error(`Initiation failed: ${initiation.error}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Step 2: Customer Identity Verification
|
|
75
|
+
const identification = await this.verifyCustomerIdentity(
|
|
76
|
+
initiation.paymentRequestId,
|
|
77
|
+
customerIdentity
|
|
78
|
+
);
|
|
79
|
+
if (!identification.ok) {
|
|
80
|
+
throw new Error(`Identification failed: ${identification.error}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 3: AML/FIU Screening
|
|
84
|
+
const amlScreening = await this.performAMLScreening(
|
|
85
|
+
initiation.paymentRequestId,
|
|
86
|
+
identification.customerData
|
|
87
|
+
);
|
|
88
|
+
if (!amlScreening.ok) {
|
|
89
|
+
throw new Error(`AML screening failed: ${amlScreening.error}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Step 4: Payment Confirmation
|
|
93
|
+
const confirmation = await this.confirmPayment(
|
|
94
|
+
initiation.paymentRequestId,
|
|
95
|
+
confirmationData
|
|
96
|
+
);
|
|
97
|
+
if (!confirmation.ok) {
|
|
98
|
+
throw new Error(`Confirmation failed: ${confirmation.error}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Step 5: TIPS Transaction Processing
|
|
102
|
+
const completion = await this.processTIPSTransaction(
|
|
103
|
+
initiation.paymentRequestId,
|
|
104
|
+
confirmation.transactionData
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
this.emit('journey:completed', { completion });
|
|
108
|
+
return completion;
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
this.emit('journey:failed', { error: error.message });
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Step 1: Merchant Payment Initiation with BoT Compliance
|
|
118
|
+
*/
|
|
119
|
+
async initiateMerchantPayment(request) {
|
|
120
|
+
const schema = z.object({
|
|
121
|
+
merchantId: z.string().min(1),
|
|
122
|
+
customerPhone: z.string().regex(/^255\d{9}$/), // Tanzanian format
|
|
123
|
+
amount: z.number().positive().max(this.regulatoryLimits.merchant.perTransaction),
|
|
124
|
+
currency: z.string().default('TZS'),
|
|
125
|
+
description: z.string().min(1).max(100),
|
|
126
|
+
reference: z.string().optional(),
|
|
127
|
+
callbackUrl: z.string().url().optional(),
|
|
128
|
+
metadata: z.object({}).optional()
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const validated = schema.parse(request);
|
|
132
|
+
|
|
133
|
+
// Verify merchant is registered and compliant
|
|
134
|
+
const merchantStatus = await this._verifyMerchantCompliance(validated.merchantId);
|
|
135
|
+
if (!merchantStatus.ok) {
|
|
136
|
+
return { ok: false, error: merchantStatus.error };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check merchant transaction limits
|
|
140
|
+
const limitCheck = await this._checkMerchantLimits(validated.merchantId, validated.amount);
|
|
141
|
+
if (!limitCheck.ok) {
|
|
142
|
+
return { ok: false, error: limitCheck.error };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Generate secure payment request ID
|
|
146
|
+
const paymentRequestId = this._generateSecurePaymentId();
|
|
147
|
+
|
|
148
|
+
// Create payment session in Redis
|
|
149
|
+
const sessionData = {
|
|
150
|
+
id: paymentRequestId,
|
|
151
|
+
merchantId: validated.merchantId,
|
|
152
|
+
customerPhone: validated.customerPhone,
|
|
153
|
+
amount: validated.amount,
|
|
154
|
+
currency: validated.currency,
|
|
155
|
+
description: validated.description,
|
|
156
|
+
reference: validated.reference || paymentRequestId,
|
|
157
|
+
status: 'INITIATED',
|
|
158
|
+
createdAt: new Date(),
|
|
159
|
+
expiresAt: new Date(Date.now() + this.regulatoryLimits.sessionTimeout),
|
|
160
|
+
callbackUrl: validated.callbackUrl,
|
|
161
|
+
metadata: validated.metadata || {}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
await this._storePaymentSession(sessionData);
|
|
165
|
+
|
|
166
|
+
this.emit('payment:initiated', sessionData);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
paymentRequestId,
|
|
171
|
+
sessionData,
|
|
172
|
+
nextStep: 'IDENTIFY_CUSTOMER',
|
|
173
|
+
expiresIn: this.regulatoryLimits.sessionTimeout / 1000
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const schema = z.object({
|
|
177
|
+
merchantId: z.string().min(1),
|
|
178
|
+
customerPhone: z.string().regex(/^255\d{9}$/), // Tanzanian format
|
|
179
|
+
amount: z.number().positive(),
|
|
180
|
+
description: z.string().min(1),
|
|
181
|
+
reference: z.string().optional(),
|
|
182
|
+
callbackUrl: z.string().url().optional()
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const validated = schema.parse(request);
|
|
186
|
+
|
|
187
|
+
// Check merchant limits
|
|
188
|
+
const merchantStatus = await this._checkMerchantLimits(
|
|
189
|
+
validated.merchantId,
|
|
190
|
+
validated.amount
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (!merchantStatus.ok) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error: merchantStatus.error,
|
|
197
|
+
code: 'MERCHANT_LIMIT_EXCEEDED'
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Generate payment request ID
|
|
202
|
+
const paymentRequestId = this._generatePaymentId();
|
|
203
|
+
|
|
204
|
+
// Store payment request
|
|
205
|
+
const paymentRequest = {
|
|
206
|
+
id: paymentRequestId,
|
|
207
|
+
merchantId: validated.merchantId,
|
|
208
|
+
customerPhone: validated.customerPhone,
|
|
209
|
+
amount: validated.amount,
|
|
210
|
+
description: validated.description,
|
|
211
|
+
reference: validated.reference || paymentRequestId,
|
|
212
|
+
status: 'INITIATED',
|
|
213
|
+
createdAt: new Date(),
|
|
214
|
+
expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 min expiry
|
|
215
|
+
callbackUrl: validated.callbackUrl
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
await this._storePaymentRequest(paymentRequest);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
paymentRequestId,
|
|
223
|
+
redirectUrl: `https://lipa.example.com/pay/${paymentRequestId}`,
|
|
224
|
+
expiresIn: 30 * 60 // seconds
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Step 2: Customer Identification (via Prompt)
|
|
230
|
+
* Customer enters PIN to identify with NIDA
|
|
231
|
+
*/
|
|
232
|
+
async identifyCustomer(paymentRequestId, nin, pin) {
|
|
233
|
+
// Get payment request
|
|
234
|
+
const paymentRequest = await this._getPaymentRequest(paymentRequestId);
|
|
235
|
+
|
|
236
|
+
if (!paymentRequest) {
|
|
237
|
+
return { ok: false, error: 'PAYMENT_REQUEST_NOT_FOUND' };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (paymentRequest.status !== 'INITIATED') {
|
|
241
|
+
return { ok: false, error: 'INVALID_PAYMENT_STATUS' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (new Date() > paymentRequest.expiresAt) {
|
|
245
|
+
return { ok: false, error: 'PAYMENT_EXPIRED' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Verify NIN with NIDA CIG
|
|
249
|
+
const nidaResult = await this._verifyNIDA(nin, pin);
|
|
250
|
+
|
|
251
|
+
if (!nidaResult.verified) {
|
|
252
|
+
return { ok: false, error: 'NIDA_VERIFICATION_FAILED' };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check customer limits
|
|
256
|
+
const customerStatus = await this._checkCustomerLimits(
|
|
257
|
+
nidaResult.nin,
|
|
258
|
+
paymentRequest.amount
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (!customerStatus.ok) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
error: customerStatus.error,
|
|
265
|
+
code: 'CUSTOMER_LIMIT_EXCEEDED'
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Update payment request with customer
|
|
270
|
+
await this._updatePaymentRequest(paymentRequestId, {
|
|
271
|
+
status: 'IDENTIFIED',
|
|
272
|
+
customerId: nidaResult.nin,
|
|
273
|
+
customerName: `${nidaResult.firstName} ${nidaResult.lastName}`,
|
|
274
|
+
identifiedAt: new Date()
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
customerId: nidaResult.nin,
|
|
280
|
+
customerName: nidaResult.customerName,
|
|
281
|
+
nextStep: 'CONFIRM'
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Step 3: AML Screening
|
|
287
|
+
* Check customer against FIU suspicious persons list
|
|
288
|
+
*/
|
|
289
|
+
async performAMLScreening(paymentRequestId, customerId) {
|
|
290
|
+
// Check FIU list
|
|
291
|
+
const isBlocked = await this._checkFIUList(customerId);
|
|
292
|
+
|
|
293
|
+
if (isBlocked) {
|
|
294
|
+
await this._updatePaymentRequest(paymentRequestId, {
|
|
295
|
+
status: 'BLOCKED',
|
|
296
|
+
blockReason: 'AML_SCREENING_FAILED'
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
error: 'AML_SCREENING_FAILED',
|
|
302
|
+
blocked: true
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Queue for transaction monitoring
|
|
307
|
+
await this._queueForMonitoring(paymentRequestId, customerId);
|
|
308
|
+
|
|
309
|
+
return { ok: true };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Step 4: Confirmation
|
|
314
|
+
* Customer confirms payment details
|
|
315
|
+
*/
|
|
316
|
+
async confirmPayment(paymentRequestId, customerId, confirmationCode) {
|
|
317
|
+
const paymentRequest = await this._getPaymentRequest(paymentRequestId);
|
|
318
|
+
|
|
319
|
+
if (!paymentRequest) {
|
|
320
|
+
return { ok: false, error: 'PAYMENT_REQUEST_NOT_FOUND' };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (paymentRequest.status !== 'IDENTIFIED') {
|
|
324
|
+
return { ok: false, error: 'INVALID_PAYMENT_STATUS' };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (paymentRequest.customerId !== customerId) {
|
|
328
|
+
return { ok: false, error: 'CUSTOMER_MISMATCH' };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Verify confirmation code (could be OTP)
|
|
332
|
+
// const isValidCode = await this._verifyConfirmationCode(customerId, confirmationCode);
|
|
333
|
+
// if (!isValidCode) {
|
|
334
|
+
// return { ok: false, error: 'INVALID_CONFIRMATION_CODE' };
|
|
335
|
+
// }
|
|
336
|
+
|
|
337
|
+
// Update payment request status
|
|
338
|
+
await this._updatePaymentRequest(paymentRequestId, {
|
|
339
|
+
status: 'CONFIRMED',
|
|
340
|
+
confirmedAt: new Date()
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
nextStep: 'PROCESS'
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Step 5: Process Payment via TIPS
|
|
351
|
+
* Submit to Tanzania Instant Payment System
|
|
352
|
+
*/
|
|
353
|
+
async processPayment(paymentRequestId, customerId) {
|
|
354
|
+
const paymentRequest = await this._getPaymentRequest(paymentRequestId);
|
|
355
|
+
|
|
356
|
+
if (!paymentRequest) {
|
|
357
|
+
return { ok: false, error: 'PAYMENT_REQUEST_NOT_FOUND' };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (paymentRequest.status !== 'CONFIRMED') {
|
|
361
|
+
return { ok: false, error: 'PAYMENT_NOT_CONFIRMED' };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
// Initiate TIPS transaction
|
|
366
|
+
const tipsResult = await this._submitToTIPS({
|
|
367
|
+
customerId,
|
|
368
|
+
merchantId: paymentRequest.merchantId,
|
|
369
|
+
amount: paymentRequest.amount,
|
|
370
|
+
reference: paymentRequest.reference,
|
|
371
|
+
description: paymentRequest.description
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (!tipsResult.ok) {
|
|
375
|
+
await this._updatePaymentRequest(paymentRequestId, {
|
|
376
|
+
status: 'FAILED',
|
|
377
|
+
failureReason: tipsResult.error
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
error: 'TIPS_TRANSACTION_FAILED'
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Update payment request
|
|
387
|
+
await this._updatePaymentRequest(paymentRequestId, {
|
|
388
|
+
status: 'COMPLETED',
|
|
389
|
+
transactionId: tipsResult.transactionId,
|
|
390
|
+
completedAt: new Date()
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Log for AML/FIU reporting
|
|
394
|
+
await this._logAMLTransaction(paymentRequestId, tipsResult.transactionId);
|
|
395
|
+
|
|
396
|
+
// Call merchant callback if configured
|
|
397
|
+
if (paymentRequest.callbackUrl) {
|
|
398
|
+
await this._notifyMerchant(paymentRequest.callbackUrl, {
|
|
399
|
+
paymentRequestId,
|
|
400
|
+
transactionId: tipsResult.transactionId,
|
|
401
|
+
status: 'COMPLETED',
|
|
402
|
+
amount: paymentRequest.amount,
|
|
403
|
+
timestamp: new Date().toISOString()
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
ok: true,
|
|
409
|
+
transactionId: tipsResult.transactionId,
|
|
410
|
+
status: 'COMPLETED',
|
|
411
|
+
amount: paymentRequest.amount,
|
|
412
|
+
reference: paymentRequest.reference
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error('[Lipa Namba] Processing error:', error);
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
error: error.message,
|
|
420
|
+
code: 'PROCESSING_ERROR'
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Full journey orchestrator
|
|
427
|
+
*/
|
|
428
|
+
async executeFullJourney(request, credentials, confirmation) {
|
|
429
|
+
const steps = [];
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
// Step 1: Initiate
|
|
433
|
+
const init = await this.initiatePayment(request);
|
|
434
|
+
if (!init.ok) return init;
|
|
435
|
+
steps.push({ step: 'INITIATE', ok: true });
|
|
436
|
+
|
|
437
|
+
// Step 2: Identify
|
|
438
|
+
const identify = await this.identifyCustomer(
|
|
439
|
+
init.paymentRequestId,
|
|
440
|
+
credentials.nin,
|
|
441
|
+
credentials.pin
|
|
442
|
+
);
|
|
443
|
+
if (!identify.ok) return identify;
|
|
444
|
+
steps.push({ step: 'IDENTIFY', ok: true });
|
|
445
|
+
|
|
446
|
+
// Step 3: AML Screen
|
|
447
|
+
const aml = await this.performAMLScreening(
|
|
448
|
+
init.paymentRequestId,
|
|
449
|
+
identify.customerId
|
|
450
|
+
);
|
|
451
|
+
if (!aml.ok) return aml;
|
|
452
|
+
steps.push({ step: 'AML_SCREEN', ok: true });
|
|
453
|
+
|
|
454
|
+
// Step 4: Confirm
|
|
455
|
+
const confirm = await this.confirmPayment(
|
|
456
|
+
init.paymentRequestId,
|
|
457
|
+
identify.customerId,
|
|
458
|
+
confirmation.code
|
|
459
|
+
);
|
|
460
|
+
if (!confirm.ok) return confirm;
|
|
461
|
+
steps.push({ step: 'CONFIRM', ok: true });
|
|
462
|
+
|
|
463
|
+
// Step 5: Process
|
|
464
|
+
const process = await this.processPayment(
|
|
465
|
+
init.paymentRequestId,
|
|
466
|
+
identify.customerId
|
|
467
|
+
);
|
|
468
|
+
if (!process.ok) return process;
|
|
469
|
+
steps.push({ step: 'PROCESS', ok: true });
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
ok: true,
|
|
473
|
+
transactionId: process.transactionId,
|
|
474
|
+
status: 'COMPLETED',
|
|
475
|
+
steps
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
} catch (error) {
|
|
479
|
+
return {
|
|
480
|
+
ok: false,
|
|
481
|
+
error: error.message,
|
|
482
|
+
completedSteps: steps
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ==================== Private Helpers ====================
|
|
488
|
+
|
|
489
|
+
async _checkMerchantLimits(merchantId, amount) {
|
|
490
|
+
// Check daily limit
|
|
491
|
+
const dailyTotal = await this._getMerchantDailyTotal(merchantId);
|
|
492
|
+
if (dailyTotal + amount > this.merchantLimits.daily) {
|
|
493
|
+
return { ok: false, error: 'DAILY_LIMIT_EXCEEDED' };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Check per-transaction limit
|
|
497
|
+
if (amount > this.merchantLimits.perTransaction) {
|
|
498
|
+
return { ok: false, error: 'TRANSACTION_LIMIT_EXCEEDED' };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return { ok: true };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async _checkCustomerLimits(customerId, amount) {
|
|
505
|
+
const dailyTotal = await this._getCustomerDailyTotal(customerId);
|
|
506
|
+
if (dailyTotal + amount > this.customerLimits.daily) {
|
|
507
|
+
return { ok: false, error: 'DAILY_LIMIT_EXCEEDED' };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (amount > this.customerLimits.perTransaction) {
|
|
511
|
+
return { ok: false, error: 'TRANSACTION_LIMIT_EXCEEDED' };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return { ok: true };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
_generatePaymentId() {
|
|
518
|
+
return `LN_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async _verifyNIDA(nin, pin) {
|
|
522
|
+
// Implementation calls NIDA CIG
|
|
523
|
+
// For now, return mock
|
|
524
|
+
return { verified: true, nin, firstName: 'John', lastName: 'Doe' };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async _checkFIUList(customerId) {
|
|
528
|
+
// Query FIU database
|
|
529
|
+
// For now, return false (not flagged)
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async _storePaymentRequest(request) {
|
|
534
|
+
// Store in database
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async _getPaymentRequest(id) {
|
|
538
|
+
// Retrieve from database
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async _updatePaymentRequest(id, updates) {
|
|
543
|
+
// Update in database
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async _getMerchantDailyTotal(merchantId) {
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async _getCustomerDailyTotal(customerId) {
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async _queueForMonitoring(paymentRequestId, customerId) {
|
|
555
|
+
// Queue for AML transaction monitoring
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async _submitToTIPS(txData) {
|
|
559
|
+
// Submit to TIPS
|
|
560
|
+
return { ok: true, transactionId: 'TIPS_' + Date.now() };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async _logAMLTransaction(paymentRequestId, transactionId) {
|
|
564
|
+
// Log for FIU reporting
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async _notifyMerchant(callbackUrl, data) {
|
|
568
|
+
// POST callback to merchant
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export default LipaNambaJourney;
|