@user231243/remove-debugger 0.0.7 → 0.0.10
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.
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="vi">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Hệ thống Thanh Toán FnB — Giải thích dễ hiểu</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--primary: #2563eb;
|
|
10
|
+
--success: #16a34a;
|
|
11
|
+
--warning: #d97706;
|
|
12
|
+
--danger: #dc2626;
|
|
13
|
+
--purple: #7c3aed;
|
|
14
|
+
--teal: #0d9488;
|
|
15
|
+
--gray: #6b7280;
|
|
16
|
+
--light: #f3f4f6;
|
|
17
|
+
--dark: #111827;
|
|
18
|
+
--card-shadow: 0 4px 24px rgba(0,0,0,0.10);
|
|
19
|
+
}
|
|
20
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
21
|
+
body {
|
|
22
|
+
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
23
|
+
background: #f0f4ff;
|
|
24
|
+
color: var(--dark);
|
|
25
|
+
line-height: 1.7;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ── HEADER ── */
|
|
29
|
+
header {
|
|
30
|
+
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 60%, #7c3aed 100%);
|
|
31
|
+
color: white;
|
|
32
|
+
padding: 48px 32px 36px;
|
|
33
|
+
text-align: center;
|
|
34
|
+
}
|
|
35
|
+
header h1 { font-size: 2.2rem; font-weight: 800; letter-spacing: -0.5px; }
|
|
36
|
+
header p { margin-top: 10px; font-size: 1.1rem; opacity: 0.85; }
|
|
37
|
+
.badge-row { margin-top: 18px; display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
|
38
|
+
.badge {
|
|
39
|
+
background: rgba(255,255,255,0.18);
|
|
40
|
+
border: 1px solid rgba(255,255,255,0.35);
|
|
41
|
+
border-radius: 999px;
|
|
42
|
+
padding: 4px 14px;
|
|
43
|
+
font-size: 0.82rem;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ── LAYOUT ── */
|
|
48
|
+
main { max-width: 1100px; margin: 0 auto; padding: 40px 20px 80px; }
|
|
49
|
+
|
|
50
|
+
/* ── SECTION ── */
|
|
51
|
+
.section { margin-bottom: 60px; }
|
|
52
|
+
.section-title {
|
|
53
|
+
display: flex; align-items: center; gap: 12px;
|
|
54
|
+
font-size: 1.5rem; font-weight: 800; color: var(--primary);
|
|
55
|
+
border-bottom: 3px solid var(--primary);
|
|
56
|
+
padding-bottom: 10px; margin-bottom: 28px;
|
|
57
|
+
}
|
|
58
|
+
.section-title .icon {
|
|
59
|
+
width: 42px; height: 42px; border-radius: 12px;
|
|
60
|
+
display: flex; align-items: center; justify-content: center;
|
|
61
|
+
font-size: 1.4rem; flex-shrink: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ── CARD ── */
|
|
65
|
+
.card {
|
|
66
|
+
background: white;
|
|
67
|
+
border-radius: 16px;
|
|
68
|
+
padding: 28px 32px;
|
|
69
|
+
box-shadow: var(--card-shadow);
|
|
70
|
+
margin-bottom: 20px;
|
|
71
|
+
}
|
|
72
|
+
.card-title {
|
|
73
|
+
font-size: 1.05rem; font-weight: 700;
|
|
74
|
+
margin-bottom: 14px; color: var(--dark);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ── GRID ── */
|
|
78
|
+
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
|
79
|
+
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
|
80
|
+
@media(max-width: 700px) {
|
|
81
|
+
.grid-2, .grid-3 { grid-template-columns: 1fr; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ── FLOW DIAGRAM ── */
|
|
85
|
+
.flow {
|
|
86
|
+
display: flex; flex-direction: column; gap: 0;
|
|
87
|
+
position: relative;
|
|
88
|
+
}
|
|
89
|
+
.flow-step {
|
|
90
|
+
display: flex; align-items: flex-start; gap: 16px;
|
|
91
|
+
position: relative;
|
|
92
|
+
padding-bottom: 28px;
|
|
93
|
+
}
|
|
94
|
+
.flow-step:last-child { padding-bottom: 0; }
|
|
95
|
+
.flow-step::before {
|
|
96
|
+
content: '';
|
|
97
|
+
position: absolute; left: 19px; top: 40px;
|
|
98
|
+
width: 2px; height: calc(100% - 12px);
|
|
99
|
+
background: #dbeafe;
|
|
100
|
+
}
|
|
101
|
+
.flow-step:last-child::before { display: none; }
|
|
102
|
+
.flow-num {
|
|
103
|
+
width: 40px; height: 40px; border-radius: 50%;
|
|
104
|
+
background: var(--primary); color: white;
|
|
105
|
+
font-weight: 800; font-size: 1rem;
|
|
106
|
+
display: flex; align-items: center; justify-content: center;
|
|
107
|
+
flex-shrink: 0; position: relative; z-index: 1;
|
|
108
|
+
}
|
|
109
|
+
.flow-content { flex: 1; padding-top: 6px; }
|
|
110
|
+
.flow-content h3 { font-size: 1rem; font-weight: 700; color: var(--dark); margin-bottom: 4px; }
|
|
111
|
+
.flow-content p { font-size: 0.92rem; color: #374151; }
|
|
112
|
+
.flow-content .code-hint {
|
|
113
|
+
margin-top: 6px;
|
|
114
|
+
font-size: 0.78rem; color: var(--gray);
|
|
115
|
+
font-family: 'Courier New', monospace;
|
|
116
|
+
background: #f3f4f6; border-radius: 6px;
|
|
117
|
+
padding: 4px 10px; display: inline-block;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── FORMULA BOX ── */
|
|
121
|
+
.formula {
|
|
122
|
+
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
|
|
123
|
+
color: white; border-radius: 14px;
|
|
124
|
+
padding: 24px 28px; margin: 20px 0;
|
|
125
|
+
font-family: 'Courier New', monospace;
|
|
126
|
+
font-size: 0.92rem; line-height: 2;
|
|
127
|
+
}
|
|
128
|
+
.formula .f-title {
|
|
129
|
+
font-family: 'Segoe UI', sans-serif;
|
|
130
|
+
font-size: 0.8rem; font-weight: 700;
|
|
131
|
+
text-transform: uppercase; letter-spacing: 1px;
|
|
132
|
+
opacity: 0.7; margin-bottom: 10px;
|
|
133
|
+
}
|
|
134
|
+
.formula .plus { color: #86efac; }
|
|
135
|
+
.formula .minus { color: #fca5a5; }
|
|
136
|
+
.formula .eq { color: #fde68a; font-weight: 800; }
|
|
137
|
+
.formula .result { color: #fde68a; font-weight: 800; font-size: 1.05rem; }
|
|
138
|
+
|
|
139
|
+
/* ── DECISION TREE ── */
|
|
140
|
+
.decision-tree { padding: 10px 0; }
|
|
141
|
+
.dt-node {
|
|
142
|
+
border-radius: 12px; padding: 14px 20px;
|
|
143
|
+
margin-bottom: 8px; position: relative;
|
|
144
|
+
}
|
|
145
|
+
.dt-root {
|
|
146
|
+
background: var(--primary); color: white;
|
|
147
|
+
font-weight: 700; font-size: 1rem; text-align: center;
|
|
148
|
+
}
|
|
149
|
+
.dt-branch-row {
|
|
150
|
+
display: flex; gap: 16px; margin: 12px 0;
|
|
151
|
+
position: relative;
|
|
152
|
+
}
|
|
153
|
+
.dt-branch-row::before {
|
|
154
|
+
content: '';
|
|
155
|
+
position: absolute; top: -12px; left: 50%;
|
|
156
|
+
transform: translateX(-50%);
|
|
157
|
+
width: 2px; height: 12px; background: #93c5fd;
|
|
158
|
+
}
|
|
159
|
+
.dt-branch {
|
|
160
|
+
flex: 1; border-radius: 10px; padding: 12px 16px;
|
|
161
|
+
border: 2px solid; font-size: 0.88rem;
|
|
162
|
+
}
|
|
163
|
+
.dt-pos { border-color: var(--success); background: #f0fdf4; }
|
|
164
|
+
.dt-neg { border-color: var(--danger); background: #fff1f2; }
|
|
165
|
+
.dt-zero { border-color: var(--warning); background: #fffbeb; }
|
|
166
|
+
.dt-label {
|
|
167
|
+
font-weight: 800; font-size: 0.75rem;
|
|
168
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
169
|
+
margin-bottom: 6px;
|
|
170
|
+
}
|
|
171
|
+
.dt-pos .dt-label { color: var(--success); }
|
|
172
|
+
.dt-neg .dt-label { color: var(--danger); }
|
|
173
|
+
.dt-zero .dt-label { color: var(--warning); }
|
|
174
|
+
.dt-branch ul { padding-left: 16px; margin-top: 6px; }
|
|
175
|
+
.dt-branch ul li { margin-bottom: 4px; font-size: 0.85rem; }
|
|
176
|
+
|
|
177
|
+
/* ── PAYMENT METHOD CARDS ── */
|
|
178
|
+
.method-card {
|
|
179
|
+
border-radius: 14px; padding: 20px;
|
|
180
|
+
border: 2px solid; text-align: center;
|
|
181
|
+
transition: transform 0.15s;
|
|
182
|
+
}
|
|
183
|
+
.method-card:hover { transform: translateY(-3px); }
|
|
184
|
+
.method-icon { font-size: 2.2rem; margin-bottom: 8px; }
|
|
185
|
+
.method-name { font-weight: 700; font-size: 0.95rem; }
|
|
186
|
+
.method-desc { font-size: 0.8rem; color: var(--gray); margin-top: 4px; }
|
|
187
|
+
.m-cash { border-color: #16a34a; background: #f0fdf4; }
|
|
188
|
+
.m-bank { border-color: #2563eb; background: #eff6ff; }
|
|
189
|
+
.m-card { border-color: #7c3aed; background: #faf5ff; }
|
|
190
|
+
.m-debt { border-color: #d97706; background: #fffbeb; }
|
|
191
|
+
.m-split { border-color: #0d9488; background: #f0fdfa; }
|
|
192
|
+
|
|
193
|
+
/* ── DIFF MONEY TABLE ── */
|
|
194
|
+
.diff-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
195
|
+
.diff-table th {
|
|
196
|
+
background: #1e3a8a; color: white;
|
|
197
|
+
padding: 10px 14px; text-align: left; font-size: 0.82rem;
|
|
198
|
+
}
|
|
199
|
+
.diff-table td { padding: 10px 14px; border-bottom: 1px solid #e5e7eb; }
|
|
200
|
+
.diff-table tr:hover td { background: #f9fafb; }
|
|
201
|
+
.diff-table tr:last-child td { border-bottom: none; }
|
|
202
|
+
.tag {
|
|
203
|
+
display: inline-block; border-radius: 6px;
|
|
204
|
+
padding: 2px 8px; font-size: 0.75rem; font-weight: 700;
|
|
205
|
+
}
|
|
206
|
+
.tag-green { background: #dcfce7; color: #15803d; }
|
|
207
|
+
.tag-red { background: #fee2e2; color: #b91c1c; }
|
|
208
|
+
.tag-yellow { background: #fef3c7; color: #92400e; }
|
|
209
|
+
.tag-blue { background: #dbeafe; color: #1d4ed8; }
|
|
210
|
+
|
|
211
|
+
/* ── SEESAW VIZ ── */
|
|
212
|
+
.seesaw-wrap {
|
|
213
|
+
display: flex; align-items: center; justify-content: center;
|
|
214
|
+
gap: 24px; padding: 20px 0; flex-wrap: wrap;
|
|
215
|
+
}
|
|
216
|
+
.seesaw-side {
|
|
217
|
+
text-align: center; flex: 1; min-width: 150px;
|
|
218
|
+
}
|
|
219
|
+
.seesaw-side .amount {
|
|
220
|
+
font-size: 1.6rem; font-weight: 800;
|
|
221
|
+
}
|
|
222
|
+
.seesaw-side .label { font-size: 0.82rem; color: var(--gray); margin-top: 4px; }
|
|
223
|
+
.seesaw-eq {
|
|
224
|
+
font-size: 2rem; font-weight: 800; color: var(--primary);
|
|
225
|
+
}
|
|
226
|
+
.seesaw-arrow {
|
|
227
|
+
display: flex; flex-direction: column; align-items: center;
|
|
228
|
+
gap: 4px; font-size: 0.8rem; color: var(--gray);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── VALIDATION CHECKLIST ── */
|
|
232
|
+
.checklist { list-style: none; }
|
|
233
|
+
.checklist li {
|
|
234
|
+
display: flex; align-items: flex-start; gap: 10px;
|
|
235
|
+
padding: 10px 0; border-bottom: 1px solid #f3f4f6;
|
|
236
|
+
font-size: 0.92rem;
|
|
237
|
+
}
|
|
238
|
+
.checklist li:last-child { border-bottom: none; }
|
|
239
|
+
.check-icon {
|
|
240
|
+
width: 26px; height: 26px; border-radius: 50%;
|
|
241
|
+
display: flex; align-items: center; justify-content: center;
|
|
242
|
+
font-size: 0.85rem; flex-shrink: 0; margin-top: 2px;
|
|
243
|
+
}
|
|
244
|
+
.check-fe { background: #dbeafe; color: #1d4ed8; }
|
|
245
|
+
.check-be { background: #dcfce7; color: #15803d; }
|
|
246
|
+
|
|
247
|
+
/* ── DEPOSIT EXCESS ── */
|
|
248
|
+
.excess-box {
|
|
249
|
+
background: linear-gradient(135deg, #7c3aed, #a855f7);
|
|
250
|
+
color: white; border-radius: 16px; padding: 28px;
|
|
251
|
+
margin-top: 20px;
|
|
252
|
+
}
|
|
253
|
+
.excess-box h3 { font-size: 1.1rem; font-weight: 800; margin-bottom: 12px; }
|
|
254
|
+
.excess-flow {
|
|
255
|
+
display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
|
|
256
|
+
margin-top: 16px;
|
|
257
|
+
}
|
|
258
|
+
.excess-step {
|
|
259
|
+
background: rgba(255,255,255,0.18);
|
|
260
|
+
border: 1px solid rgba(255,255,255,0.3);
|
|
261
|
+
border-radius: 10px; padding: 12px 16px;
|
|
262
|
+
font-size: 0.85rem; flex: 1; min-width: 140px;
|
|
263
|
+
text-align: center;
|
|
264
|
+
}
|
|
265
|
+
.excess-arrow { font-size: 1.4rem; opacity: 0.7; }
|
|
266
|
+
|
|
267
|
+
/* ── SUMMARY TIMELINE ── */
|
|
268
|
+
.timeline { position: relative; padding-left: 32px; }
|
|
269
|
+
.timeline::before {
|
|
270
|
+
content: ''; position: absolute; left: 9px; top: 0; bottom: 0;
|
|
271
|
+
width: 2px; background: linear-gradient(to bottom, #2563eb, #7c3aed);
|
|
272
|
+
}
|
|
273
|
+
.tl-item { position: relative; margin-bottom: 24px; }
|
|
274
|
+
.tl-dot {
|
|
275
|
+
position: absolute; left: -28px; top: 4px;
|
|
276
|
+
width: 16px; height: 16px; border-radius: 50%;
|
|
277
|
+
border: 3px solid white; flex-shrink: 0;
|
|
278
|
+
box-shadow: 0 0 0 2px var(--primary);
|
|
279
|
+
background: var(--primary);
|
|
280
|
+
}
|
|
281
|
+
.tl-item h4 { font-weight: 700; font-size: 0.95rem; }
|
|
282
|
+
.tl-item p { font-size: 0.88rem; color: #374151; margin-top: 3px; }
|
|
283
|
+
.tl-time {
|
|
284
|
+
display: inline-block; font-size: 0.72rem;
|
|
285
|
+
background: #eff6ff; color: var(--primary);
|
|
286
|
+
border-radius: 6px; padding: 2px 8px;
|
|
287
|
+
font-weight: 700; margin-top: 4px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* ── CALLOUT ── */
|
|
291
|
+
.callout {
|
|
292
|
+
border-left: 4px solid; border-radius: 0 10px 10px 0;
|
|
293
|
+
padding: 14px 18px; margin: 16px 0; font-size: 0.9rem;
|
|
294
|
+
}
|
|
295
|
+
.callout-info { border-color: var(--primary); background: #eff6ff; }
|
|
296
|
+
.callout-warn { border-color: var(--warning); background: #fffbeb; }
|
|
297
|
+
.callout-success { border-color: var(--success); background: #f0fdf4; }
|
|
298
|
+
.callout strong { display: block; margin-bottom: 4px; }
|
|
299
|
+
|
|
300
|
+
/* ── NAV ── */
|
|
301
|
+
nav.toc {
|
|
302
|
+
background: white; border-radius: 14px;
|
|
303
|
+
padding: 20px 24px; margin-bottom: 36px;
|
|
304
|
+
box-shadow: var(--card-shadow);
|
|
305
|
+
}
|
|
306
|
+
nav.toc h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 1px; color: var(--gray); margin-bottom: 12px; }
|
|
307
|
+
nav.toc ol { padding-left: 20px; }
|
|
308
|
+
nav.toc li { margin-bottom: 6px; }
|
|
309
|
+
nav.toc a { color: var(--primary); text-decoration: none; font-size: 0.92rem; font-weight: 600; }
|
|
310
|
+
nav.toc a:hover { text-decoration: underline; }
|
|
311
|
+
|
|
312
|
+
/* ── MISC ── */
|
|
313
|
+
code { background: #f3f4f6; border-radius: 5px; padding: 2px 7px; font-size: 0.85rem; font-family: 'Courier New', monospace; color: #7c3aed; }
|
|
314
|
+
.text-center { text-align: center; }
|
|
315
|
+
.mt-12 { margin-top: 12px; }
|
|
316
|
+
.mt-20 { margin-top: 20px; }
|
|
317
|
+
|
|
318
|
+
/* SVG flowchart */
|
|
319
|
+
.svg-flow-wrap { overflow-x: auto; padding: 10px 0; }
|
|
320
|
+
.svg-flow-wrap svg { min-width: 700px; }
|
|
321
|
+
</style>
|
|
322
|
+
</head>
|
|
323
|
+
<body>
|
|
324
|
+
|
|
325
|
+
<header>
|
|
326
|
+
<h1>🍽️ Hệ Thống Thanh Toán FnB</h1>
|
|
327
|
+
<p>Giải thích từ A→Z theo luồng thực tế trong mã nguồn — ngôn ngữ dễ hiểu, có hình ảnh minh họa</p>
|
|
328
|
+
<div class="badge-row">
|
|
329
|
+
<span class="badge">📂 _BillOrderModal.js</span>
|
|
330
|
+
<span class="badge">📂 Index.js</span>
|
|
331
|
+
<span class="badge">📂 OrdersAppService.cs</span>
|
|
332
|
+
<span class="badge">📂 OrderValidator.cs</span>
|
|
333
|
+
</div>
|
|
334
|
+
</header>
|
|
335
|
+
|
|
336
|
+
<main>
|
|
337
|
+
|
|
338
|
+
<!-- TOC -->
|
|
339
|
+
<nav class="toc">
|
|
340
|
+
<h2>Mục lục</h2>
|
|
341
|
+
<ol>
|
|
342
|
+
<li><a href="#s1">Luồng tổng quan — toàn bộ quá trình</a></li>
|
|
343
|
+
<li><a href="#s2">Bước 1 — Mở màn hình thanh toán</a></li>
|
|
344
|
+
<li><a href="#s3">Bước 2 — Tính tiền: Công thức cốt lõi</a></li>
|
|
345
|
+
<li><a href="#s4">Bước 3 — Phương thức thanh toán</a></li>
|
|
346
|
+
<li><a href="#s5">Bước 4 — Xử lý tiền thừa / thiếu</a></li>
|
|
347
|
+
<li><a href="#s6">Bước 5 — Tiền cọc dư (Deposit Excess)</a></li>
|
|
348
|
+
<li><a href="#s7">Bước 6 — Xác nhận & lưu thanh toán</a></li>
|
|
349
|
+
<li><a href="#s8">Logic server: Double-check độc lập</a></li>
|
|
350
|
+
</ol>
|
|
351
|
+
</nav>
|
|
352
|
+
|
|
353
|
+
<!-- ────────────────────────── SECTION 1: TỔNG QUAN ────────────────────────── -->
|
|
354
|
+
<section class="section" id="s1">
|
|
355
|
+
<div class="section-title">
|
|
356
|
+
<div class="icon" style="background:#dbeafe">🗺️</div>
|
|
357
|
+
Luồng tổng quan — toàn bộ quá trình
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<div class="card">
|
|
361
|
+
<div class="card-title">Sơ đồ luồng thanh toán từ đầu đến cuối</div>
|
|
362
|
+
|
|
363
|
+
<div class="svg-flow-wrap">
|
|
364
|
+
<svg viewBox="0 0 900 160" xmlns="http://www.w3.org/2000/svg" style="height:160px">
|
|
365
|
+
<defs>
|
|
366
|
+
<marker id="arr" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
|
367
|
+
<path d="M0,0 L0,6 L8,3 z" fill="#6b7280"/>
|
|
368
|
+
</marker>
|
|
369
|
+
</defs>
|
|
370
|
+
<!-- nodes -->
|
|
371
|
+
<!-- 1 -->
|
|
372
|
+
<rect x="10" y="55" width="110" height="50" rx="10" fill="#2563eb"/>
|
|
373
|
+
<text x="65" y="77" text-anchor="middle" fill="white" font-size="11" font-weight="bold">Nhấn</text>
|
|
374
|
+
<text x="65" y="91" text-anchor="middle" fill="white" font-size="11" font-weight="bold">Thanh toán</text>
|
|
375
|
+
<!-- 2 -->
|
|
376
|
+
<rect x="148" y="55" width="110" height="50" rx="10" fill="#0d9488"/>
|
|
377
|
+
<text x="203" y="77" text-anchor="middle" fill="white" font-size="11" font-weight="bold">Load đơn</text>
|
|
378
|
+
<text x="203" y="91" text-anchor="middle" fill="white" font-size="10">+ cài đặt thuế</text>
|
|
379
|
+
<!-- 3 -->
|
|
380
|
+
<rect x="286" y="55" width="110" height="50" rx="10" fill="#7c3aed"/>
|
|
381
|
+
<text x="341" y="77" text-anchor="middle" fill="white" font-size="11" font-weight="bold">Tính tiền</text>
|
|
382
|
+
<text x="341" y="91" text-anchor="middle" fill="white" font-size="10">phải trả</text>
|
|
383
|
+
<!-- 4 -->
|
|
384
|
+
<rect x="424" y="55" width="110" height="50" rx="10" fill="#d97706"/>
|
|
385
|
+
<text x="479" y="77" text-anchor="middle" fill="white" font-size="11" font-weight="bold">Chọn PT</text>
|
|
386
|
+
<text x="479" y="91" text-anchor="middle" fill="white" font-size="10">thanh toán</text>
|
|
387
|
+
<!-- 5 -->
|
|
388
|
+
<rect x="562" y="55" width="110" height="50" rx="10" fill="#dc2626"/>
|
|
389
|
+
<text x="617" y="77" text-anchor="middle" fill="white" font-size="11" font-weight="bold">Phân bổ</text>
|
|
390
|
+
<text x="617" y="91" text-anchor="middle" fill="white" font-size="10">tiền thừa/thiếu</text>
|
|
391
|
+
<!-- 6 -->
|
|
392
|
+
<rect x="700" y="55" width="110" height="50" rx="10" fill="#16a34a"/>
|
|
393
|
+
<text x="755" y="77" text-anchor="middle" fill="white" font-size="11" font-weight="bold">Xác nhận</text>
|
|
394
|
+
<text x="755" y="91" text-anchor="middle" fill="white" font-size="10">& Lưu</text>
|
|
395
|
+
<!-- 7 -->
|
|
396
|
+
<rect x="820" y="55" width="70" height="50" rx="10" fill="#1e3a8a"/>
|
|
397
|
+
<text x="855" y="77" text-anchor="middle" fill="white" font-size="11" font-weight="bold">Done</text>
|
|
398
|
+
<text x="855" y="91" text-anchor="middle" fill="white" font-size="10">✅ Paid</text>
|
|
399
|
+
<!-- arrows -->
|
|
400
|
+
<line x1="120" y1="80" x2="145" y2="80" stroke="#6b7280" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
401
|
+
<line x1="258" y1="80" x2="283" y2="80" stroke="#6b7280" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
402
|
+
<line x1="396" y1="80" x2="421" y2="80" stroke="#6b7280" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
403
|
+
<line x1="534" y1="80" x2="559" y2="80" stroke="#6b7280" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
404
|
+
<line x1="672" y1="80" x2="697" y2="80" stroke="#6b7280" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
405
|
+
<line x1="810" y1="80" x2="817" y2="80" stroke="#6b7280" stroke-width="1.5" marker-end="url(#arr)"/>
|
|
406
|
+
<!-- labels -->
|
|
407
|
+
<text x="65" y="120" text-anchor="middle" fill="#6b7280" font-size="9">Index.js:267</text>
|
|
408
|
+
<text x="203" y="120" text-anchor="middle" fill="#6b7280" font-size="9">getByIdFnB()</text>
|
|
409
|
+
<text x="341" y="120" text-anchor="middle" fill="#6b7280" font-size="9">calcTotalAmountPayable()</text>
|
|
410
|
+
<text x="479" y="120" text-anchor="middle" fill="#6b7280" font-size="9">_BillOrderModal</text>
|
|
411
|
+
<text x="617" y="120" text-anchor="middle" fill="#6b7280" font-size="9">updateDifferenceMoney()</text>
|
|
412
|
+
<text x="755" y="120" text-anchor="middle" fill="#6b7280" font-size="9">save() → API</text>
|
|
413
|
+
<text x="855" y="120" text-anchor="middle" fill="#6b7280" font-size="9">Status=Paid</text>
|
|
414
|
+
</svg>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<div class="callout callout-info mt-12">
|
|
418
|
+
<strong>💡 Nguyên tắc cốt lõi</strong>
|
|
419
|
+
Hệ thống không đơn giản là "thu đủ tiền". Nó bắt buộc <strong>mọi khoản chênh lệch phải được phân bổ rõ ràng</strong> — tiền thừa đi đâu, tiền thiếu ai chịu, không được "lơ lửng".
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</section>
|
|
423
|
+
|
|
424
|
+
<!-- ────────────────────────── SECTION 2: MỞ MODAL ────────────────────────── -->
|
|
425
|
+
<section class="section" id="s2">
|
|
426
|
+
<div class="section-title">
|
|
427
|
+
<div class="icon" style="background:#dbeafe">🚪</div>
|
|
428
|
+
Bước 1 — Mở màn hình thanh toán
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div class="grid-2">
|
|
432
|
+
<div class="card">
|
|
433
|
+
<div class="card-title">Luồng khởi tạo từng bước</div>
|
|
434
|
+
<div class="flow">
|
|
435
|
+
<div class="flow-step">
|
|
436
|
+
<div class="flow-num">1</div>
|
|
437
|
+
<div class="flow-content">
|
|
438
|
+
<h3>Nhân viên bấm nút "Thanh toán"</h3>
|
|
439
|
+
<p>Button <code>.btn-payment</code> gọi <code>openPaymentModal()</code>. Trước khi mở modal, hệ thống gọi API lấy toàn bộ chi tiết đơn hàng để kiểm tra (bàn có món bị hủy chưa xử lý không?).</p>
|
|
440
|
+
<span class="code-hint">Index.js:267 → Index.js:456</span>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="flow-step">
|
|
444
|
+
<div class="flow-num">2</div>
|
|
445
|
+
<div class="flow-content">
|
|
446
|
+
<h3>Modal mở — gọi 3 API cùng lúc</h3>
|
|
447
|
+
<p>Dùng <code>Promise.all</code> để tải song song 3 nguồn dữ liệu, không chờ tuần tự.</p>
|
|
448
|
+
<span class="code-hint">_BillOrderModal.js:70-83</span>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
<div class="flow-step">
|
|
452
|
+
<div class="flow-num">3</div>
|
|
453
|
+
<div class="flow-content">
|
|
454
|
+
<h3>Ba API được gọi song song</h3>
|
|
455
|
+
<p>① <strong>getByIdFnB()</strong> — đơn hàng đầy đủ (món, vòng order, khách hàng)<br>
|
|
456
|
+
② <strong>getPaymentMethodComboboxItem()</strong> — danh sách hình thức thanh toán<br>
|
|
457
|
+
③ <strong>getByKeys()</strong> — cài đặt thuế, phụ thu</p>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="flow-step">
|
|
461
|
+
<div class="flow-num">4</div>
|
|
462
|
+
<div class="flow-content">
|
|
463
|
+
<h3>Tính toán & hiển thị</h3>
|
|
464
|
+
<p>Sau khi có dữ liệu: chạy <code>calculateTotalAmountPayable()</code> → <code>updateDifferenceMoney()</code> → gắn validator → gắn sự kiện.</p>
|
|
465
|
+
<span class="code-hint">_BillOrderModal.js:45-324</span>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<div class="card">
|
|
472
|
+
<div class="card-title">Dữ liệu truyền vào modal</div>
|
|
473
|
+
<div class="callout callout-warn">
|
|
474
|
+
<strong>⚠️ Quan trọng</strong>
|
|
475
|
+
Modal chỉ nhận <code>serviceTurnId</code> — không nhận dữ liệu đơn hàng trực tiếp. Toàn bộ dữ liệu được <strong>fetch lại từ server</strong> bên trong <code>initForm()</code>.
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<div class="mt-20">
|
|
479
|
+
<div style="background:#f9fafb;border-radius:10px;padding:16px;font-size:0.88rem">
|
|
480
|
+
<div style="font-weight:700;margin-bottom:10px">📦 Sau khi initForm() xong, modal có:</div>
|
|
481
|
+
<div style="display:flex;flex-direction:column;gap:8px">
|
|
482
|
+
<div style="display:flex;gap:8px;align-items:flex-start">
|
|
483
|
+
<span style="color:var(--success);font-weight:700">✓</span>
|
|
484
|
+
<div>Danh sách <strong>tất cả món</strong> trong đơn (kể cả nhiều vòng order)</div>
|
|
485
|
+
</div>
|
|
486
|
+
<div style="display:flex;gap:8px;align-items:flex-start">
|
|
487
|
+
<span style="color:var(--success);font-weight:700">✓</span>
|
|
488
|
+
<div>Danh sách <strong>hình thức thanh toán</strong> của tenant</div>
|
|
489
|
+
</div>
|
|
490
|
+
<div style="display:flex;gap:8px;align-items:flex-start">
|
|
491
|
+
<span style="color:var(--success);font-weight:700">✓</span>
|
|
492
|
+
<div>Cài đặt <strong>thuế, phụ thu</strong> của nhà hàng</div>
|
|
493
|
+
</div>
|
|
494
|
+
<div style="display:flex;gap:8px;align-items:flex-start">
|
|
495
|
+
<span style="color:var(--success);font-weight:700">✓</span>
|
|
496
|
+
<div>Thông tin <strong>tiền cọc</strong> đã đặt trước</div>
|
|
497
|
+
</div>
|
|
498
|
+
<div style="display:flex;gap:8px;align-items:flex-start">
|
|
499
|
+
<span style="color:var(--success);font-weight:700">✓</span>
|
|
500
|
+
<div>Các <strong>khuyến mãi</strong> đang áp dụng</div>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
</section>
|
|
508
|
+
|
|
509
|
+
<!-- ────────────────────────── SECTION 3: TÍNH TIỀN ────────────────────────── -->
|
|
510
|
+
<section class="section" id="s3">
|
|
511
|
+
<div class="section-title">
|
|
512
|
+
<div class="icon" style="background:#ede9fe">🧮</div>
|
|
513
|
+
Bước 2 — Tính tiền: Công thức cốt lõi
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
<div class="card">
|
|
517
|
+
<div class="card-title">Công thức tính "Tiền khách cần trả"</div>
|
|
518
|
+
|
|
519
|
+
<div class="formula">
|
|
520
|
+
<div class="f-title">calculateTotalAmountPayable() — _BillOrderModal.js:672</div>
|
|
521
|
+
<span class="plus">totalAmount</span> ← Tổng giá tất cả món<br>
|
|
522
|
+
<span class="minus">- totalDiscount</span> ← Trừ khuyến mãi (CTKM)<br>
|
|
523
|
+
<span class="minus">- costOfSaleTotalAmount</span> ← Trừ giảm giá bán thêm<br>
|
|
524
|
+
<span class="plus">+ VAT</span> ← Cộng thuế VAT<br>
|
|
525
|
+
<span class="minus">- depositAmount</span> ← Trừ tiền cọc đã đặt<br>
|
|
526
|
+
<span class="plus">+ otherCostTotalAmount</span> ← Cộng phụ thu khác<br>
|
|
527
|
+
<span class="eq">= totalAmountPayable</span> ← Tiền khách cần trả
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<div class="grid-2" style="margin-top:20px">
|
|
531
|
+
<div>
|
|
532
|
+
<div class="card-title">Sàn giá: Không bao giờ âm</div>
|
|
533
|
+
<div style="background:#f9fafb;border-radius:10px;padding:16px;font-family:'Courier New',monospace;font-size:0.9rem">
|
|
534
|
+
<div style="color:#dc2626;font-weight:700">// Hiển thị trên bill (L141)</div>
|
|
535
|
+
renderData.totalAmountPayable =<br>
|
|
536
|
+
<span style="color:#2563eb">Math.max(0, totalAmountPayable)</span><br><br>
|
|
537
|
+
<div style="color:#dc2626;font-weight:700">// Tổng phải thu (L680)</div>
|
|
538
|
+
totalPayment = totalAmountPayable > 0<br>
|
|
539
|
+
? totalAmountPayable<br>
|
|
540
|
+
: <span style="color:#2563eb">0</span>
|
|
541
|
+
</div>
|
|
542
|
+
<div class="callout callout-warn mt-12">
|
|
543
|
+
Khi cọc > hóa đơn → <code>totalAmountPayable < 0</code> → kích hoạt chế độ <strong>hoàn cọc dư</strong> (xem Bước 5).
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
<div>
|
|
547
|
+
<div class="card-title">Hàm trigger khi nào?</div>
|
|
548
|
+
<ul style="list-style:none;font-size:0.9rem">
|
|
549
|
+
<li style="padding:8px 0;border-bottom:1px solid #f3f4f6">
|
|
550
|
+
<span class="tag tag-blue">initForm</span> Khi modal vừa mở xong (L100)
|
|
551
|
+
</li>
|
|
552
|
+
<li style="padding:8px 0;border-bottom:1px solid #f3f4f6">
|
|
553
|
+
<span class="tag tag-yellow">onChange</span> Khi nhân viên đổi số tiền, phương thức (L667)
|
|
554
|
+
</li>
|
|
555
|
+
<li style="padding:8px 0;border-bottom:1px solid #f3f4f6">
|
|
556
|
+
<span class="tag tag-green">promo</span> Khi áp dụng / bỏ khuyến mãi
|
|
557
|
+
</li>
|
|
558
|
+
<li style="padding:8px 0">
|
|
559
|
+
<span class="tag tag-red">save</span> Trước khi lưu để kiểm tra lần cuối
|
|
560
|
+
</li>
|
|
561
|
+
</ul>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
</section>
|
|
566
|
+
|
|
567
|
+
<!-- ────────────────────────── SECTION 4: PHƯƠNG THỨC ────────────────────────── -->
|
|
568
|
+
<section class="section" id="s4">
|
|
569
|
+
<div class="section-title">
|
|
570
|
+
<div class="icon" style="background:#fef3c7">💳</div>
|
|
571
|
+
Bước 3 — Phương thức thanh toán
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
<div class="grid-3" style="margin-bottom:20px">
|
|
575
|
+
<div class="method-card m-cash">
|
|
576
|
+
<div class="method-icon">💵</div>
|
|
577
|
+
<div class="method-name">Tiền mặt</div>
|
|
578
|
+
<div class="method-desc">TienMat (enum 0)<br>Phổ biến nhất, có thể hoàn cọc</div>
|
|
579
|
+
</div>
|
|
580
|
+
<div class="method-card m-bank">
|
|
581
|
+
<div class="method-icon">📱</div>
|
|
582
|
+
<div class="method-name">Chuyển khoản / QR</div>
|
|
583
|
+
<div class="method-desc">ChuyenKhoan (enum 1)<br>Chọn ngân hàng, hiện QR</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="method-card m-card">
|
|
586
|
+
<div class="method-icon">💳</div>
|
|
587
|
+
<div class="method-name">Quẹt thẻ</div>
|
|
588
|
+
<div class="method-desc">QuetThe (enum 2)<br>Chọn máy POS / tài khoản</div>
|
|
589
|
+
</div>
|
|
590
|
+
<div class="method-card m-debt">
|
|
591
|
+
<div class="method-icon">📋</div>
|
|
592
|
+
<div class="method-name">Công nợ (Charge to Account)</div>
|
|
593
|
+
<div class="method-desc">Khách nợ, trả sau<br>Không phải dòng payment</div>
|
|
594
|
+
</div>
|
|
595
|
+
<div class="method-card m-split">
|
|
596
|
+
<div class="method-icon">➕</div>
|
|
597
|
+
<div class="method-name">Kết hợp nhiều PT</div>
|
|
598
|
+
<div class="method-desc">"Khác" → mở modal riêng<br>N phương thức + N số tiền</div>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
<div class="card">
|
|
603
|
+
<div class="card-title">Thanh toán kết hợp nhiều phương thức (Split Payment)</div>
|
|
604
|
+
<div class="grid-2">
|
|
605
|
+
<div>
|
|
606
|
+
<p style="font-size:0.92rem;margin-bottom:14px">Khi khách chọn <strong>"Khác"</strong>, modal <code>_PaymentMethodMultipleModal.js</code> mở ra. Nhân viên có thể thêm nhiều dòng:</p>
|
|
607
|
+
|
|
608
|
+
<div style="border:2px dashed #0d9488;border-radius:12px;padding:16px;font-size:0.85rem">
|
|
609
|
+
<div style="font-weight:700;color:#0d9488;margin-bottom:10px">Ví dụ: Hóa đơn 500,000đ</div>
|
|
610
|
+
<table style="width:100%;border-collapse:collapse">
|
|
611
|
+
<tr style="background:#f0fdfa">
|
|
612
|
+
<th style="padding:6px;text-align:left;font-size:0.8rem">Phương thức</th>
|
|
613
|
+
<th style="padding:6px;text-align:right;font-size:0.8rem">Số tiền</th>
|
|
614
|
+
</tr>
|
|
615
|
+
<tr>
|
|
616
|
+
<td style="padding:6px">💵 Tiền mặt</td>
|
|
617
|
+
<td style="padding:6px;text-align:right">200,000đ</td>
|
|
618
|
+
</tr>
|
|
619
|
+
<tr style="background:#f9fafb">
|
|
620
|
+
<td style="padding:6px">📱 Chuyển khoản (VCB)</td>
|
|
621
|
+
<td style="padding:6px;text-align:right">300,000đ</td>
|
|
622
|
+
</tr>
|
|
623
|
+
<tr style="font-weight:700;color:#0d9488">
|
|
624
|
+
<td style="padding:6px">Tổng</td>
|
|
625
|
+
<td style="padding:6px;text-align:right">500,000đ</td>
|
|
626
|
+
</tr>
|
|
627
|
+
</table>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
<div>
|
|
631
|
+
<div class="card-title">Validation từng dòng thanh toán</div>
|
|
632
|
+
<ul class="checklist">
|
|
633
|
+
<li>
|
|
634
|
+
<span class="check-icon check-fe">FE</span>
|
|
635
|
+
Phải chọn loại hình thức thanh toán
|
|
636
|
+
</li>
|
|
637
|
+
<li>
|
|
638
|
+
<span class="check-icon check-fe">FE</span>
|
|
639
|
+
Nếu là ngân hàng/thẻ → phải chọn tài khoản/máy POS
|
|
640
|
+
</li>
|
|
641
|
+
<li>
|
|
642
|
+
<span class="check-icon check-fe">FE</span>
|
|
643
|
+
Số tiền mỗi dòng > 0
|
|
644
|
+
</li>
|
|
645
|
+
<li>
|
|
646
|
+
<span class="check-icon check-be">BE</span>
|
|
647
|
+
Không dòng nào (trừ dòng cuối) được vượt quá số tiền còn lại
|
|
648
|
+
</li>
|
|
649
|
+
<li>
|
|
650
|
+
<span class="check-icon check-be">BE</span>
|
|
651
|
+
<code>OrderValidator.cs:1531</code> — kiểm tra từng payment theo thứ tự
|
|
652
|
+
</li>
|
|
653
|
+
</ul>
|
|
654
|
+
|
|
655
|
+
<div class="callout callout-info mt-12">
|
|
656
|
+
<strong>💡 Công nợ ≠ Payment</strong>
|
|
657
|
+
Công nợ không tạo dòng <code>orderPayments</code>. Nó được lưu vào trường <code>DServiceTurn.ChargeToAccount</code> riêng biệt — là "khoản nợ của khách", không phải "tiền đã thu".
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
</section>
|
|
663
|
+
|
|
664
|
+
<!-- ────────────────────────── SECTION 5: TIỀN THỪA/THIẾU ────────────────────────── -->
|
|
665
|
+
<section class="section" id="s5">
|
|
666
|
+
<div class="section-title">
|
|
667
|
+
<div class="icon" style="background:#fee2e2">⚖️</div>
|
|
668
|
+
Bước 4 — Xử lý tiền thừa / thiếu
|
|
669
|
+
</div>
|
|
670
|
+
|
|
671
|
+
<div class="card">
|
|
672
|
+
<div class="card-title">Nguyên tắc "Bảo toàn chênh lệch"</div>
|
|
673
|
+
|
|
674
|
+
<div class="seesaw-wrap">
|
|
675
|
+
<div class="seesaw-side">
|
|
676
|
+
<div class="amount" style="color:var(--primary)">Tiền khách đưa</div>
|
|
677
|
+
<div class="label">totalPayment</div>
|
|
678
|
+
</div>
|
|
679
|
+
<div class="seesaw-arrow">
|
|
680
|
+
<div style="font-size:1.5rem">−</div>
|
|
681
|
+
</div>
|
|
682
|
+
<div class="seesaw-side">
|
|
683
|
+
<div class="amount" style="color:var(--danger)">Tiền cần trả</div>
|
|
684
|
+
<div class="label">totalAmountPayable</div>
|
|
685
|
+
</div>
|
|
686
|
+
<div class="seesaw-eq">=</div>
|
|
687
|
+
<div class="seesaw-side">
|
|
688
|
+
<div class="amount" style="color:var(--warning)">Chênh lệch</div>
|
|
689
|
+
<div class="label">moneyChange</div>
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
<div class="callout callout-success">
|
|
694
|
+
<strong>📐 Bất biến quan trọng (OrderValidator.cs:411, 460)</strong>
|
|
695
|
+
Tổng các trường phân bổ <strong>PHẢI BẰNG</strong> chênh lệch. Không được dư, không được thiếu — phải bằng đúng.
|
|
696
|
+
</div>
|
|
697
|
+
|
|
698
|
+
<div class="decision-tree mt-20">
|
|
699
|
+
<div class="dt-node dt-root">moneyChange = totalPayment − totalAmountPayable</div>
|
|
700
|
+
|
|
701
|
+
<div class="dt-branch-row">
|
|
702
|
+
<div class="dt-branch dt-pos">
|
|
703
|
+
<div class="dt-label">▲ Thừa tiền (moneyChange > 0)</div>
|
|
704
|
+
<div style="font-size:0.85rem;margin-bottom:8px">Khách đưa nhiều hơn hóa đơn. Phải phân bổ số tiền thừa vào <em>đúng một hoặc nhiều</em> trong 3 lựa chọn sau:</div>
|
|
705
|
+
<ul>
|
|
706
|
+
<li>💰 <strong>Tiền thừa trả khách</strong> — hoàn tiền mặt</li>
|
|
707
|
+
<li>📋 <strong>Tính vào công nợ</strong> — dùng số dư làm tín dụng lần sau</li>
|
|
708
|
+
<li>🎁 <strong>Khách không lấy tiền thừa</strong> — khách tặng lại nhà hàng</li>
|
|
709
|
+
</ul>
|
|
710
|
+
<div style="margin-top:8px;padding:8px;background:rgba(22,163,74,0.1);border-radius:8px;font-size:0.8rem">
|
|
711
|
+
<strong>Bất biến:</strong> tiền thừa trả + công nợ + khách không lấy = moneyChange
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
|
|
715
|
+
<div class="dt-branch dt-neg">
|
|
716
|
+
<div class="dt-label">▼ Thiếu tiền (moneyChange < 0)</div>
|
|
717
|
+
<div style="font-size:0.85rem;margin-bottom:8px">Khách đưa ít hơn hóa đơn. Hệ thống <em>vẫn cho phép</em> nếu phần thiếu được phân bổ vào:</div>
|
|
718
|
+
<ul>
|
|
719
|
+
<li>📋 <strong>Tính vào công nợ</strong> — khách nợ nhà hàng</li>
|
|
720
|
+
<li>🎁 <strong>Bớt tiền lẻ cho khách</strong> — nhà hàng làm tròn giảm</li>
|
|
721
|
+
</ul>
|
|
722
|
+
<div style="margin-top:8px;padding:8px;background:rgba(220,38,38,0.1);border-radius:8px;font-size:0.8rem">
|
|
723
|
+
<strong>Bất biến:</strong> công nợ + bớt tiền lẻ = |moneyChange|
|
|
724
|
+
<br><strong>Không được dùng:</strong> tiền thừa trả / khách không lấy
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
<div class="dt-branch dt-zero">
|
|
729
|
+
<div class="dt-label">= Khớp đúng (moneyChange = 0)</div>
|
|
730
|
+
<div style="font-size:0.85rem">Khách đưa đúng số tiền hóa đơn. Không cần phân bổ gì thêm. Lý tưởng nhất!</div>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
|
|
736
|
+
<div class="card">
|
|
737
|
+
<div class="card-title">Bảng tra cứu 4 trường phân bổ</div>
|
|
738
|
+
<table class="diff-table">
|
|
739
|
+
<thead>
|
|
740
|
+
<tr>
|
|
741
|
+
<th>Tên FE</th>
|
|
742
|
+
<th>Tên Backend</th>
|
|
743
|
+
<th>Ý nghĩa</th>
|
|
744
|
+
<th>Khi nào dùng</th>
|
|
745
|
+
</tr>
|
|
746
|
+
</thead>
|
|
747
|
+
<tbody>
|
|
748
|
+
<tr>
|
|
749
|
+
<td><code>changeReturned</code></td>
|
|
750
|
+
<td>TienThuaTraKhach</td>
|
|
751
|
+
<td>Tiền thừa trả lại khách bằng tiền mặt</td>
|
|
752
|
+
<td><span class="tag tag-green">Chỉ thừa</span></td>
|
|
753
|
+
</tr>
|
|
754
|
+
<tr>
|
|
755
|
+
<td><code>chargeToAccount</code></td>
|
|
756
|
+
<td>TinhVaoCongNo</td>
|
|
757
|
+
<td>Ghi vào công nợ khách hàng</td>
|
|
758
|
+
<td><span class="tag tag-yellow">Cả hai</span></td>
|
|
759
|
+
</tr>
|
|
760
|
+
<tr>
|
|
761
|
+
<td><code>unclaimedChange</code></td>
|
|
762
|
+
<td>KhachKhongLayTienThua</td>
|
|
763
|
+
<td>Khách tặng lại, không lấy tiền thừa</td>
|
|
764
|
+
<td><span class="tag tag-green">Chỉ thừa</span></td>
|
|
765
|
+
</tr>
|
|
766
|
+
<tr>
|
|
767
|
+
<td><code>roundOffDiscount</code></td>
|
|
768
|
+
<td>BotTienLeChoKhach</td>
|
|
769
|
+
<td>Nhà hàng làm tròn, giảm tiền lẻ cho khách</td>
|
|
770
|
+
<td><span class="tag tag-red">Chỉ thiếu</span></td>
|
|
771
|
+
</tr>
|
|
772
|
+
</tbody>
|
|
773
|
+
</table>
|
|
774
|
+
</div>
|
|
775
|
+
</section>
|
|
776
|
+
|
|
777
|
+
<!-- ────────────────────────── SECTION 6: CỌC DƯ ────────────────────────── -->
|
|
778
|
+
<section class="section" id="s6">
|
|
779
|
+
<div class="section-title">
|
|
780
|
+
<div class="icon" style="background:#ede9fe">🏦</div>
|
|
781
|
+
Bước 5 — Tiền cọc dư (Deposit Excess)
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<div class="card">
|
|
785
|
+
<div class="card-title">Khi nào xảy ra?</div>
|
|
786
|
+
<div class="grid-2">
|
|
787
|
+
<div style="font-size:0.92rem">
|
|
788
|
+
<p>Khách đặt bàn và cọc trước <strong>500,000đ</strong>. Nhưng khi ăn xong, hóa đơn chỉ có <strong>300,000đ</strong>.</p>
|
|
789
|
+
<div class="formula" style="margin-top:14px;font-size:0.88rem">
|
|
790
|
+
<span class="minus">totalAmountPayable</span> = 300,000 − 500,000 = <span class="result">−200,000</span>
|
|
791
|
+
</div>
|
|
792
|
+
<div class="callout callout-warn" style="margin-top:12px">
|
|
793
|
+
Khi <code>totalAmountPayable < 0</code> → biến <code>isDepositExcess = true</code> được bật. Toàn bộ luồng hoàn tiền được kích hoạt.
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
<div>
|
|
797
|
+
<div style="background:#7c3aed;color:white;border-radius:12px;padding:18px;font-size:0.88rem">
|
|
798
|
+
<div style="font-weight:700;margin-bottom:10px">📐 Logic tính hoàn cọc (L863-896)</div>
|
|
799
|
+
<div style="font-family:'Courier New',monospace;line-height:1.8">
|
|
800
|
+
excessAmount = |totalAmountPayable|<br>
|
|
801
|
+
<span style="color:#c4b5fd">// = 200,000đ cần hoàn</span><br><br>
|
|
802
|
+
refundPayment = clamp(<br>
|
|
803
|
+
input, 0, excessAmount<br>
|
|
804
|
+
)<br>
|
|
805
|
+
<span style="color:#c4b5fd">// Giới hạn [0 .. 200,000]</span><br><br>
|
|
806
|
+
chargeToAccount =<br>
|
|
807
|
+
excessAmount − refundPayment<br>
|
|
808
|
+
<span style="color:#c4b5fd">// Phần còn lại → công nợ</span>
|
|
809
|
+
</div>
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
<div class="excess-box">
|
|
815
|
+
<h3>📦 Ví dụ: Cọc 500k, hóa đơn 300k → Dư 200k</h3>
|
|
816
|
+
<div class="excess-flow">
|
|
817
|
+
<div class="excess-step">
|
|
818
|
+
<div style="font-size:1.5rem">🏦</div>
|
|
819
|
+
<div style="font-weight:700">Cọc: 500,000đ</div>
|
|
820
|
+
<div style="opacity:0.8;font-size:0.8rem">Đã đặt trước</div>
|
|
821
|
+
</div>
|
|
822
|
+
<div class="excess-arrow">→</div>
|
|
823
|
+
<div class="excess-step">
|
|
824
|
+
<div style="font-size:1.5rem">🧾</div>
|
|
825
|
+
<div style="font-weight:700">Hóa đơn: 300,000đ</div>
|
|
826
|
+
<div style="opacity:0.8;font-size:0.8rem">Thực tế ăn</div>
|
|
827
|
+
</div>
|
|
828
|
+
<div class="excess-arrow">→</div>
|
|
829
|
+
<div class="excess-step" style="border-color:rgba(255,255,255,0.6)">
|
|
830
|
+
<div style="font-size:1.5rem">⚡</div>
|
|
831
|
+
<div style="font-weight:700">Dư: 200,000đ</div>
|
|
832
|
+
<div style="opacity:0.8;font-size:0.8rem">isDepositExcess = true</div>
|
|
833
|
+
</div>
|
|
834
|
+
<div class="excess-arrow">→</div>
|
|
835
|
+
<div class="excess-step">
|
|
836
|
+
<div style="font-size:1.5rem">💵</div>
|
|
837
|
+
<div style="font-weight:700">Hoàn tiền: 200,000đ</div>
|
|
838
|
+
<div style="opacity:0.8;font-size:0.8rem">Tiền mặt / chuyển khoản</div>
|
|
839
|
+
</div>
|
|
840
|
+
<div class="excess-arrow">hoặc</div>
|
|
841
|
+
<div class="excess-step">
|
|
842
|
+
<div style="font-size:1.5rem">📋</div>
|
|
843
|
+
<div style="font-weight:700">Công nợ: 200,000đ</div>
|
|
844
|
+
<div style="opacity:0.8;font-size:0.8rem">Tín dụng lần sau</div>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
<div class="callout callout-info" style="margin-top:20px">
|
|
850
|
+
<strong>🔒 Giới hạn phương thức hoàn cọc</strong>
|
|
851
|
+
Chỉ <strong>Tiền mặt</strong> và <strong>Chuyển khoản</strong> được dùng để hoàn cọc. Thẻ tín dụng và các phương thức khác không được hỗ trợ (<code>isDepositExcessPaymentMethod</code>, L32).
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
</section>
|
|
855
|
+
|
|
856
|
+
<!-- ────────────────────────── SECTION 7: LƯU ────────────────────────── -->
|
|
857
|
+
<section class="section" id="s7">
|
|
858
|
+
<div class="section-title">
|
|
859
|
+
<div class="icon" style="background:#dcfce7">✅</div>
|
|
860
|
+
Bước 6 — Xác nhận & lưu thanh toán
|
|
861
|
+
</div>
|
|
862
|
+
|
|
863
|
+
<div class="card">
|
|
864
|
+
<div class="card-title">Luồng khi nhân viên bấm "Xác nhận"</div>
|
|
865
|
+
|
|
866
|
+
<div class="timeline">
|
|
867
|
+
<div class="tl-item">
|
|
868
|
+
<div class="tl-dot"></div>
|
|
869
|
+
<h4>Validate phía Frontend</h4>
|
|
870
|
+
<p><code>$form.valid()</code> — kiểm tra tất cả trường bắt buộc. Nếu tenant tắt "Áp dụng gộp CTKM" → chặn nếu có >1 khuyến mãi đang chọn.</p>
|
|
871
|
+
<span class="tl-time">_BillOrderModal.js:573-590</span>
|
|
872
|
+
</div>
|
|
873
|
+
<div class="tl-item">
|
|
874
|
+
<div class="tl-dot" style="background:var(--teal);box-shadow:0 0 0 2px var(--teal)"></div>
|
|
875
|
+
<h4>API #1 — Cập nhật món tặng</h4>
|
|
876
|
+
<p>Gọi <code>createOrEditFnBForMobile()</code> để convert món khuyến mãi sang trạng thái cuối trước khi thanh toán. Bước chuẩn bị.</p>
|
|
877
|
+
<span class="tl-time">_BillOrderModal.js:644</span>
|
|
878
|
+
</div>
|
|
879
|
+
<div class="tl-item">
|
|
880
|
+
<div class="tl-dot" style="background:var(--purple);box-shadow:0 0 0 2px var(--purple)"></div>
|
|
881
|
+
<h4>API #2 — Lưu thanh toán</h4>
|
|
882
|
+
<p>Gọi <code>POST /api/services/app/Orders/CreateOrEditOrderPaymentFnb</code> với toàn bộ payload: serviceTurnId, các dòng payment, số tiền, phân bổ chênh lệch.</p>
|
|
883
|
+
<span class="tl-time">_BillOrderModal.js:645</span>
|
|
884
|
+
</div>
|
|
885
|
+
<div class="tl-item">
|
|
886
|
+
<div class="tl-dot" style="background:var(--warning);box-shadow:0 0 0 2px var(--warning)"></div>
|
|
887
|
+
<h4>Server double-check & lưu DB</h4>
|
|
888
|
+
<p>Server tự tính lại <code>totalAmountPayable</code> và kiểm tra có khớp với dữ liệu client gửi lên không. Nếu không khớp → lỗi. Cập nhật DServiceTurn: Status=Done, PaymentStatus=Paid, FinishedTime=now.</p>
|
|
889
|
+
<span class="tl-time">OrderValidator.cs:1499 | OrdersAppService.cs:189-236</span>
|
|
890
|
+
</div>
|
|
891
|
+
<div class="tl-item">
|
|
892
|
+
<div class="tl-dot" style="background:var(--success);box-shadow:0 0 0 2px var(--success)"></div>
|
|
893
|
+
<h4>Hoàn tất — UI cập nhật</h4>
|
|
894
|
+
<p>Toast "Thanh toán thành công" hiện ra. Modal đóng. Danh sách đơn hàng reload. Bàn chuyển trạng thái Paid ✅. In receipt là bước <em>riêng biệt</em> — nhân viên bấm nút in thủ công.</p>
|
|
895
|
+
<span class="tl-time">_BillOrderModal.js:651-654 | Index.js:488-501</span>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
</div>
|
|
899
|
+
|
|
900
|
+
<div class="card">
|
|
901
|
+
<div class="card-title">📦 Payload gửi lên server gồm những gì?</div>
|
|
902
|
+
<div class="grid-2">
|
|
903
|
+
<div>
|
|
904
|
+
<div class="card-title" style="font-size:0.88rem;color:var(--gray)">Thông tin đơn hàng</div>
|
|
905
|
+
<ul style="list-style:none;font-size:0.88rem">
|
|
906
|
+
<li style="padding:5px 0;border-bottom:1px solid #f3f4f6">🔑 <code>serviceTurnId</code> — ID của lượt phục vụ</li>
|
|
907
|
+
<li style="padding:5px 0;border-bottom:1px solid #f3f4f6">👤 <code>customerId</code> — ID khách hàng</li>
|
|
908
|
+
<li style="padding:5px 0;border-bottom:1px solid #f3f4f6">💰 <code>totalAmountPayable</code> — Tiền cần trả (theo FE)</li>
|
|
909
|
+
<li style="padding:5px 0">💳 <code>totalPayment</code> — Tổng tiền khách đưa</li>
|
|
910
|
+
</ul>
|
|
911
|
+
</div>
|
|
912
|
+
<div>
|
|
913
|
+
<div class="card-title" style="font-size:0.88rem;color:var(--gray)">Phân bổ & thanh toán</div>
|
|
914
|
+
<ul style="list-style:none;font-size:0.88rem">
|
|
915
|
+
<li style="padding:5px 0;border-bottom:1px solid #f3f4f6">📋 <code>orderPayments[]</code> — Danh sách dòng thanh toán</li>
|
|
916
|
+
<li style="padding:5px 0;border-bottom:1px solid #f3f4f6">💱 <code>changeReturned</code> — Tiền thừa trả khách</li>
|
|
917
|
+
<li style="padding:5px 0;border-bottom:1px solid #f3f4f6">🔖 <code>chargeToAccount</code> — Công nợ</li>
|
|
918
|
+
<li style="padding:5px 0;border-bottom:1px solid #f3f4f6">🎁 <code>unclaimedChange</code> — Khách không lấy tiền thừa</li>
|
|
919
|
+
<li style="padding:5px 0;border-bottom:1px solid #f3f4f6">🎰 <code>roundOffDiscount</code> — Bớt tiền lẻ</li>
|
|
920
|
+
<li style="padding:5px 0">🏦 <code>refundPayment</code> — Hoàn cọc (nếu dư)</li>
|
|
921
|
+
</ul>
|
|
922
|
+
</div>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
</section>
|
|
926
|
+
|
|
927
|
+
<!-- ────────────────────────── SECTION 8: SERVER DOUBLE-CHECK ────────────────────────── -->
|
|
928
|
+
<section class="section" id="s8">
|
|
929
|
+
<div class="section-title">
|
|
930
|
+
<div class="icon" style="background:#fef3c7">🔍</div>
|
|
931
|
+
Logic server: Double-check độc lập
|
|
932
|
+
</div>
|
|
933
|
+
|
|
934
|
+
<div class="card">
|
|
935
|
+
<div class="card-title">Tại sao server cần tính lại?</div>
|
|
936
|
+
<div class="callout callout-warn">
|
|
937
|
+
<strong>⚠️ Không tin tưởng hoàn toàn dữ liệu từ client</strong>
|
|
938
|
+
Nếu chỉ dựa vào số liệu FE gửi lên, hacker hoặc lỗi phần mềm có thể tạo ra hóa đơn với số tiền sai. Server tính độc lập và so sánh.
|
|
939
|
+
</div>
|
|
940
|
+
|
|
941
|
+
<div class="grid-2" style="margin-top:20px">
|
|
942
|
+
<div>
|
|
943
|
+
<div class="card-title">Server tự tính (OrderValidator.cs:1486-1499)</div>
|
|
944
|
+
<div class="formula" style="font-size:0.82rem">
|
|
945
|
+
<div class="f-title">Recompute totalAmountPayable</div>
|
|
946
|
+
<span class="plus">TongTien</span><br>
|
|
947
|
+
<span class="minus">− GiamGia</span><br>
|
|
948
|
+
<span class="minus">− TaxReduction</span><br>
|
|
949
|
+
<span class="plus">+ Tax</span><br>
|
|
950
|
+
<span class="plus">+ OtherCost</span><br>
|
|
951
|
+
<span class="minus">− DepositPaid</span><br>
|
|
952
|
+
<span class="eq">= server_totalAmountPayable</span>
|
|
953
|
+
</div>
|
|
954
|
+
</div>
|
|
955
|
+
<div>
|
|
956
|
+
<div class="card-title">So sánh kết quả</div>
|
|
957
|
+
<div style="font-size:0.9rem">
|
|
958
|
+
<div style="background:#f0fdf4;border:2px solid #16a34a;border-radius:10px;padding:16px;margin-bottom:12px">
|
|
959
|
+
<div style="color:#15803d;font-weight:700;margin-bottom:6px">✅ Khớp → Cho phép lưu</div>
|
|
960
|
+
<code>server_total == input.SoTienCanTra</code>
|
|
961
|
+
</div>
|
|
962
|
+
<div style="background:#fff1f2;border:2px solid #dc2626;border-radius:10px;padding:16px">
|
|
963
|
+
<div style="color:#b91c1c;font-weight:700;margin-bottom:6px">❌ Không khớp → Từ chối</div>
|
|
964
|
+
<code>server_total != input.SoTienCanTra</code><br>
|
|
965
|
+
<span style="font-size:0.82rem;color:var(--gray);margin-top:4px;display:block">Trả về lỗi validation</span>
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
|
|
969
|
+
<div class="callout callout-info" style="margin-top:12px">
|
|
970
|
+
<strong>📐 Bất biến phân bổ cũng được kiểm tra</strong>
|
|
971
|
+
Server kiểm tra: tổng các trường phân bổ = chênh lệch. Không được lệch 1 đồng.
|
|
972
|
+
</div>
|
|
973
|
+
</div>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
|
|
977
|
+
<div class="card">
|
|
978
|
+
<div class="card-title">Kết quả cuối — Những gì được lưu vào DB</div>
|
|
979
|
+
<table class="diff-table">
|
|
980
|
+
<thead>
|
|
981
|
+
<tr>
|
|
982
|
+
<th>Trường</th>
|
|
983
|
+
<th>Bảng DB</th>
|
|
984
|
+
<th>Giá trị</th>
|
|
985
|
+
</tr>
|
|
986
|
+
</thead>
|
|
987
|
+
<tbody>
|
|
988
|
+
<tr>
|
|
989
|
+
<td>ChangeReturned, ChargeToAccount, UnclaimedChange, RoundOffDiscount</td>
|
|
990
|
+
<td>DServiceTurn</td>
|
|
991
|
+
<td>Phân bổ chênh lệch tiền</td>
|
|
992
|
+
</tr>
|
|
993
|
+
<tr>
|
|
994
|
+
<td>RefundPayment</td>
|
|
995
|
+
<td>DServiceTurn</td>
|
|
996
|
+
<td>Số tiền hoàn cọc (nếu có)</td>
|
|
997
|
+
</tr>
|
|
998
|
+
<tr>
|
|
999
|
+
<td>orderPayments[]</td>
|
|
1000
|
+
<td>ThanhToanDonHang</td>
|
|
1001
|
+
<td>Các dòng tiền thực thu (tiền mặt, chuyển khoản...)</td>
|
|
1002
|
+
</tr>
|
|
1003
|
+
<tr>
|
|
1004
|
+
<td>Status</td>
|
|
1005
|
+
<td>DServiceTurn</td>
|
|
1006
|
+
<td><span class="tag tag-green">Done</span></td>
|
|
1007
|
+
</tr>
|
|
1008
|
+
<tr>
|
|
1009
|
+
<td>PaymentStatus</td>
|
|
1010
|
+
<td>DServiceTurn</td>
|
|
1011
|
+
<td><span class="tag tag-green">Paid</span></td>
|
|
1012
|
+
</tr>
|
|
1013
|
+
<tr>
|
|
1014
|
+
<td>FinishedTime</td>
|
|
1015
|
+
<td>DServiceTurn</td>
|
|
1016
|
+
<td>Thời điểm xác nhận thanh toán</td>
|
|
1017
|
+
</tr>
|
|
1018
|
+
</tbody>
|
|
1019
|
+
</table>
|
|
1020
|
+
</div>
|
|
1021
|
+
</section>
|
|
1022
|
+
|
|
1023
|
+
<!-- FOOTER NOTE -->
|
|
1024
|
+
<div class="card text-center" style="background:linear-gradient(135deg,#1e3a8a,#7c3aed);color:white;padding:32px">
|
|
1025
|
+
<div style="font-size:1.5rem;margin-bottom:8px">🎯</div>
|
|
1026
|
+
<div style="font-size:1.1rem;font-weight:800;margin-bottom:10px">Tóm tắt bằng 1 câu</div>
|
|
1027
|
+
<div style="font-size:0.95rem;opacity:0.9;max-width:700px;margin:0 auto">
|
|
1028
|
+
Hệ thống thanh toán FnB không chỉ "thu tiền" — nó bắt buộc <strong>mọi đồng chênh lệch phải được khai báo rõ ràng</strong>, server kiểm tra độc lập với FE, và toàn bộ luồng từ nhấn nút đến lưu DB là <strong>xác định, không mơ hồ</strong>.
|
|
1029
|
+
</div>
|
|
1030
|
+
<div style="margin-top:16px;font-size:0.78rem;opacity:0.65">
|
|
1031
|
+
Tổng hợp từ phân tích 4 agent song song — _BillOrderModal.js · Index.js · OrdersAppService.cs · OrderValidator.cs
|
|
1032
|
+
</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
|
|
1035
|
+
</main>
|
|
1036
|
+
</body>
|
|
1037
|
+
</html>
|