Commit c4315f5e2b984420816d92ddcb1c548738b8df31
1 parent
5621a6057f
Exists in
master
resume begitu ada ussd response
Showing 1 changed file with 3 additions and 0 deletions Inline Diff
lib/partner-mkios.js
1 | "use strict"; | 1 | "use strict"; |
2 | 2 | ||
3 | const moment = require('moment'); | 3 | const moment = require('moment'); |
4 | 4 | ||
5 | const Modem = require('./modem'); | 5 | const Modem = require('./modem'); |
6 | 6 | ||
7 | const pullgw = require('komodo-sdk/gateway/pull'); | 7 | const pullgw = require('komodo-sdk/gateway/pull'); |
8 | 8 | ||
9 | const config = require('komodo-sdk/config'); | 9 | const config = require('komodo-sdk/config'); |
10 | const logger = require('komodo-sdk/logger'); | 10 | const logger = require('komodo-sdk/logger'); |
11 | const matrix = require('komodo-sdk/matrix'); | 11 | const matrix = require('komodo-sdk/matrix'); |
12 | 12 | ||
13 | const modemDashboard = require('./modem-dashboard'); | 13 | const modemDashboard = require('./modem-dashboard'); |
14 | 14 | ||
15 | if (config && config.debug_modem) { | 15 | if (config && config.debug_modem) { |
16 | process.env.KOMODO_DEBUG_MODEM=1; | 16 | process.env.KOMODO_DEBUG_MODEM=1; |
17 | } | 17 | } |
18 | 18 | ||
19 | if (!config || !config.partner || !config.partner.pin) { | 19 | if (!config || !config.partner || !config.partner.pin) { |
20 | logger.warn('Undefined PIN'); | 20 | logger.warn('Undefined PIN'); |
21 | process.exit(1); | 21 | process.exit(1); |
22 | } | 22 | } |
23 | 23 | ||
24 | matrix.modem = {}; | 24 | matrix.modem = {}; |
25 | matrix.not_ready = true; | 25 | matrix.not_ready = true; |
26 | matrix.stock = {}; | 26 | matrix.stock = {}; |
27 | 27 | ||
28 | const db = require('./local-db').getConnection(); | 28 | const db = require('./local-db').getConnection(); |
29 | const pendingArchive = require('./pending-archive'); | 29 | const pendingArchive = require('./pending-archive'); |
30 | const patternMatcher = require('./pattern-rule-matcher'); | 30 | const patternMatcher = require('./pattern-rule-matcher'); |
31 | const smsHandler = require('./sms-handler'); | 31 | const smsHandler = require('./sms-handler'); |
32 | 32 | ||
33 | const modem = new Modem(config.partner.modem.dev, {baudRate: 115200}); | 33 | const modem = new Modem(config.partner.modem.dev, {baudRate: 115200}); |
34 | 34 | ||
35 | const resumeHandlers = {}; | 35 | const resumeHandlers = {}; |
36 | 36 | ||
37 | let last_trx_id = null; | 37 | let last_trx_id = null; |
38 | 38 | ||
39 | modem.on('open', function() { | 39 | modem.on('open', function() { |
40 | logger.info('Modem opened'); | 40 | logger.info('Modem opened'); |
41 | 41 | ||
42 | const ussd_command = '*776*' + config.partner.pin + '#'; | 42 | const ussd_command = '*776*' + config.partner.pin + '#'; |
43 | db.run("INSERT INTO ussd VALUES (?, ?, 'OUT', ?, ?)", moment().format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD'), matrix.modem.imsi, 'AT+CUSD=1,"' + ussd_command + '",15', function(err) { | 43 | db.run("INSERT INTO ussd VALUES (?, ?, 'OUT', ?, ?)", moment().format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD'), matrix.modem.imsi, 'AT+CUSD=1,"' + ussd_command + '",15', function(err) { |
44 | if (err) { | 44 | if (err) { |
45 | logger.warn('Error inserting ussd command (stock check) to local database', {err: err}); | 45 | logger.warn('Error inserting ussd command (stock check) to local database', {err: err}); |
46 | } | 46 | } |
47 | }); | 47 | }); |
48 | 48 | ||
49 | modem.sendUSSD(ussd_command, function() {}); | 49 | modem.sendUSSD(ussd_command, function() {}); |
50 | }) | 50 | }) |
51 | 51 | ||
52 | modem.on('imsi', function(imsi) { | 52 | modem.on('imsi', function(imsi) { |
53 | logger.verbose('IMSI: ' + imsi); | 53 | logger.verbose('IMSI: ' + imsi); |
54 | matrix.modem.imsi = imsi; | 54 | matrix.modem.imsi = imsi; |
55 | }) | 55 | }) |
56 | 56 | ||
57 | 57 | ||
58 | function onIncomingSMS(sms) { | 58 | function onIncomingSMS(sms) { |
59 | logger.info('Incoming SMS', {sms: sms}); | 59 | logger.info('Incoming SMS', {sms: sms}); |
60 | db.run("INSERT INTO sms VALUES (?, ?, 'IN', ?, ?, ?)", sms.created, moment(sms.created).format('YYYY-MM-DD'), matrix.modem.imsi, sms.sender, sms.msg, function(err) { | 60 | db.run("INSERT INTO sms VALUES (?, ?, 'IN', ?, ?, ?)", sms.created, moment(sms.created).format('YYYY-MM-DD'), matrix.modem.imsi, sms.sender, sms.msg, function(err) { |
61 | if (err) { | 61 | if (err) { |
62 | logger.warn('Error inserting sms to local database', {err: err}); | 62 | logger.warn('Error inserting sms to local database', {err: err}); |
63 | } | 63 | } |
64 | }); | 64 | }); |
65 | 65 | ||
66 | if (!smsHandler.isAllowedSender(sms.sender)) { | 66 | if (!smsHandler.isAllowedSender(sms.sender)) { |
67 | logger.verbose('Ignoring SMS from unknown sender', {sender: sms.sender}); | 67 | logger.verbose('Ignoring SMS from unknown sender', {sender: sms.sender}); |
68 | return; | 68 | return; |
69 | } | 69 | } |
70 | 70 | ||
71 | const stocks = smsHandler.getMultiStockBalance(sms.msg); | 71 | const stocks = smsHandler.getMultiStockBalance(sms.msg); |
72 | if (stocks && Array.isArray(stocks) && stocks.length) { | 72 | if (stocks && Array.isArray(stocks) && stocks.length) { |
73 | stocks.forEach(function(stock) { | 73 | stocks.forEach(function(stock) { |
74 | const vals = stock.split('='); | 74 | const vals = stock.split('='); |
75 | updateStock(vals[0], vals[1]); | 75 | updateStock(vals[0], vals[1]); |
76 | }) | 76 | }) |
77 | } | 77 | } |
78 | else { | 78 | else { |
79 | const stock = smsHandler.getStockBalance(sms.msg); | 79 | const stock = smsHandler.getStockBalance(sms.msg); |
80 | if (stock.name && stock.balance) { | 80 | if (stock.name && stock.balance) { |
81 | updateStock(stock.name, stock.balance); | 81 | updateStock(stock.name, stock.balance); |
82 | } | 82 | } |
83 | } | 83 | } |
84 | 84 | ||
85 | const destination = smsHandler.getDestination(sms.msg); | 85 | const destination = smsHandler.getDestination(sms.msg); |
86 | if (!destination) { | 86 | if (!destination) { |
87 | logger.verbose('Ignoring SMS with unknown trx destination'); | 87 | logger.verbose('Ignoring SMS with unknown trx destination'); |
88 | return; | 88 | return; |
89 | } | 89 | } |
90 | 90 | ||
91 | const product = smsHandler.getProduct(sms.msg); | 91 | const product = smsHandler.getProduct(sms.msg); |
92 | if (!product) { | 92 | if (!product) { |
93 | logger.verbose('Ignoring SMS with unknown trx product'); | 93 | logger.verbose('Ignoring SMS with unknown trx product'); |
94 | return; | 94 | return; |
95 | } | 95 | } |
96 | 96 | ||
97 | const trx_date = smsHandler.getTrxDate(sms.msg); | 97 | const trx_date = smsHandler.getTrxDate(sms.msg); |
98 | if (!trx_date) { | 98 | if (!trx_date) { |
99 | logger.verbose('Ignoring SMS with unknown trx date'); | 99 | logger.verbose('Ignoring SMS with unknown trx date'); |
100 | return; | 100 | return; |
101 | } | 101 | } |
102 | 102 | ||
103 | logger.verbose('SMS message parsed and extracted', {destination: destination, product: product, trx_date: trx_date}); | 103 | logger.verbose('SMS message parsed and extracted', {destination: destination, product: product, trx_date: trx_date}); |
104 | pendingArchive.get(destination, product, trx_date, function(err, trx_id) { | 104 | pendingArchive.get(destination, product, trx_date, function(err, trx_id) { |
105 | if (!trx_id) { | 105 | if (!trx_id) { |
106 | logger.verbose('No pending trx suits with SMS', {destination: destination, product: product, trx_date: trx_date}); | 106 | logger.verbose('No pending trx suits with SMS', {destination: destination, product: product, trx_date: trx_date}); |
107 | return; | 107 | return; |
108 | } | 108 | } |
109 | 109 | ||
110 | deleteResumeHandler(trx_id); | 110 | deleteResumeHandler(trx_id); |
111 | pendingArchive.remove(trx_id); | 111 | pendingArchive.remove(trx_id); |
112 | 112 | ||
113 | report({ | 113 | report({ |
114 | trx_id: trx_id, | 114 | trx_id: trx_id, |
115 | rc: smsHandler.getRc(sms.msg) || '68', | 115 | rc: smsHandler.getRc(sms.msg) || '68', |
116 | sn: smsHandler.getSn(sms.msg), | 116 | sn: smsHandler.getSn(sms.msg), |
117 | message: 'SMS: ' + sms.msg | 117 | message: 'SMS: ' + sms.msg |
118 | }) | 118 | }) |
119 | }) | 119 | }) |
120 | 120 | ||
121 | } | 121 | } |
122 | modem.on('incoming sms', onIncomingSMS); | 122 | modem.on('incoming sms', onIncomingSMS); |
123 | 123 | ||
124 | modem.on('signal strength', function(signal_strength) { | 124 | modem.on('signal strength', function(signal_strength) { |
125 | matrix.modem.signal_strength = signal_strength; | 125 | matrix.modem.signal_strength = signal_strength; |
126 | logger.verbose('Signal strength: ' + signal_strength); | 126 | logger.verbose('Signal strength: ' + signal_strength); |
127 | }) | 127 | }) |
128 | 128 | ||
129 | function onUSSDResponse(data) { | 129 | function onUSSDResponse(data) { |
130 | logger.verbose('Got USSD response', {response: data}); | 130 | logger.verbose('Got USSD response', {response: data}); |
131 | 131 | ||
132 | db.run("INSERT INTO ussd VALUES (?, ?, 'IN', ?, ?)", moment().format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD'), matrix.modem.imsi, data, function(err) { | 132 | db.run("INSERT INTO ussd VALUES (?, ?, 'IN', ?, ?)", moment().format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD'), matrix.modem.imsi, data, function(err) { |
133 | if (err) { | 133 | if (err) { |
134 | logger.warn('Error inserting ussd response to local database', {err: err}); | 134 | logger.warn('Error inserting ussd response to local database', {err: err}); |
135 | } | 135 | } |
136 | }); | 136 | }); |
137 | 137 | ||
138 | 138 | ||
139 | if (!last_trx_id) return; | 139 | if (!last_trx_id) return; |
140 | 140 | ||
141 | 141 | ||
142 | const rc = getRcFromMessage(data, config.ussd_parser.rc) || '68'; | 142 | const rc = getRcFromMessage(data, config.ussd_parser.rc) || '68'; |
143 | if (rc !== '68') { | 143 | if (rc !== '68') { |
144 | onTrxFinish(last_trx_id); | 144 | onTrxFinish(last_trx_id); |
145 | } | 145 | } |
146 | 146 | ||
147 | deleteResumeHandler(last_trx_id);; | ||
148 | matrix.not_ready = false; | ||
149 | |||
147 | report({ | 150 | report({ |
148 | trx_id: last_trx_id, | 151 | trx_id: last_trx_id, |
149 | rc: rc, | 152 | rc: rc, |
150 | sn: getSnFromMessage(data), | 153 | sn: getSnFromMessage(data), |
151 | message: data | 154 | message: data |
152 | }); | 155 | }); |
153 | 156 | ||
154 | const stock_name = getStockProductFromMessage(data); | 157 | const stock_name = getStockProductFromMessage(data); |
155 | const stock_balance = getStockBalanceFromMesssage(data); | 158 | const stock_balance = getStockBalanceFromMesssage(data); |
156 | 159 | ||
157 | updateStock(stock_name, stock_balance); | 160 | updateStock(stock_name, stock_balance); |
158 | last_trx_id = null; | 161 | last_trx_id = null; |
159 | } | 162 | } |
160 | 163 | ||
161 | modem.on('ussd response', onUSSDResponse); | 164 | modem.on('ussd response', onUSSDResponse); |
162 | 165 | ||
163 | 166 | ||
164 | logger.info('Opening MODEM'); | 167 | logger.info('Opening MODEM'); |
165 | modem.open(function(err) { | 168 | modem.open(function(err) { |
166 | if (err) { | 169 | if (err) { |
167 | logger.warn('Error opening modem port', {err: err}); | 170 | logger.warn('Error opening modem port', {err: err}); |
168 | process.exit(1); | 171 | process.exit(1); |
169 | } | 172 | } |
170 | 173 | ||
171 | logger.info('Modem open successfully, going to ready in 30 secs'); | 174 | logger.info('Modem open successfully, going to ready in 30 secs'); |
172 | setTimeout( | 175 | setTimeout( |
173 | function() { | 176 | function() { |
174 | matrix.not_ready = false; | 177 | matrix.not_ready = false; |
175 | logger.info('Gateway is ready'); | 178 | logger.info('Gateway is ready'); |
176 | }, | 179 | }, |
177 | 30 * 1000 | 180 | 30 * 1000 |
178 | ) | 181 | ) |
179 | 182 | ||
180 | }) | 183 | }) |
181 | 184 | ||
182 | function updateStock(stock_name, stock_balance) { | 185 | function updateStock(stock_name, stock_balance) { |
183 | if (stock_name && (stock_balance !== undefined || stock_balance !== null)) { | 186 | if (stock_name && (stock_balance !== undefined || stock_balance !== null)) { |
184 | logger.verbose('Updating stock', {stock_name: stock_name, stock_balance: stock_balance}); | 187 | logger.verbose('Updating stock', {stock_name: stock_name, stock_balance: stock_balance}); |
185 | 188 | ||
186 | const new_stock_name = config && config.remote_product_alias && config.remote_product_alias[stock_name] ? config.remote_product_alias[stock_name] : stock_name; | 189 | const new_stock_name = config && config.remote_product_alias && config.remote_product_alias[stock_name] ? config.remote_product_alias[stock_name] : stock_name; |
187 | 190 | ||
188 | matrix.stock[new_stock_name] = Number(stock_balance); | 191 | matrix.stock[new_stock_name] = Number(stock_balance); |
189 | 192 | ||
190 | logger.verbose('Stock balance updated', {stock: matrix.stock}); | 193 | logger.verbose('Stock balance updated', {stock: matrix.stock}); |
191 | } | 194 | } |
192 | } | 195 | } |
193 | 196 | ||
194 | function getRcFromMessage(msg, rules) { | 197 | function getRcFromMessage(msg, rules) { |
195 | if (!rules || !Array.isArray(rules)) { | 198 | if (!rules || !Array.isArray(rules)) { |
196 | return '68'; | 199 | return '68'; |
197 | } | 200 | } |
198 | 201 | ||
199 | for (let rule of rules) { | 202 | for (let rule of rules) { |
200 | if (!rule.pattern) return '68'; | 203 | if (!rule.pattern) return '68'; |
201 | 204 | ||
202 | const re = new RegExp(rule.pattern); | 205 | const re = new RegExp(rule.pattern); |
203 | if (msg.search(re) >= 0) { | 206 | if (msg.search(re) >= 0) { |
204 | return rule.rc ? rule.rc : '68'; | 207 | return rule.rc ? rule.rc : '68'; |
205 | } | 208 | } |
206 | } | 209 | } |
207 | 210 | ||
208 | return '68'; | 211 | return '68'; |
209 | } | 212 | } |
210 | 213 | ||
211 | function getPatternMatchFromMessage(msg, rules) { | 214 | function getPatternMatchFromMessage(msg, rules) { |
212 | if (!rules || !Array.isArray(rules)) { | 215 | if (!rules || !Array.isArray(rules)) { |
213 | return; | 216 | return; |
214 | } | 217 | } |
215 | 218 | ||
216 | for (let rule of rules) { | 219 | for (let rule of rules) { |
217 | if (!rule.pattern) return; | 220 | if (!rule.pattern) return; |
218 | 221 | ||
219 | const re = new RegExp(rule.pattern); | 222 | const re = new RegExp(rule.pattern); |
220 | const matches = msg.match(re); | 223 | const matches = msg.match(re); |
221 | 224 | ||
222 | if (matches && matches.length >= 2) { | 225 | if (matches && matches.length >= 2) { |
223 | return matches[1]; | 226 | return matches[1]; |
224 | } | 227 | } |
225 | } | 228 | } |
226 | } | 229 | } |
227 | 230 | ||
228 | function getSnFromMessage(msg, rules) { | 231 | function getSnFromMessage(msg, rules) { |
229 | return patternMatcher(msg, config.ussd_parser.sn); | 232 | return patternMatcher(msg, config.ussd_parser.sn); |
230 | } | 233 | } |
231 | 234 | ||
232 | function getStockProductFromMessage(msg, rules) { | 235 | function getStockProductFromMessage(msg, rules) { |
233 | return patternMatcher(msg, config.ussd_parser.stock.product); | 236 | return patternMatcher(msg, config.ussd_parser.stock.product); |
234 | } | 237 | } |
235 | 238 | ||
236 | function getStockBalanceFromMesssage(msg, rules) { | 239 | function getStockBalanceFromMesssage(msg, rules) { |
237 | return patternMatcher(msg, config.ussd_parser.stock.balance); | 240 | return patternMatcher(msg, config.ussd_parser.stock.balance); |
238 | } | 241 | } |
239 | 242 | ||
240 | function suspendPull(trx_id) { | 243 | function suspendPull(trx_id) { |
241 | logger.verbose('Set modem to not ready so no other task can be entered and registering delayed resume'); | 244 | logger.verbose('Set modem to not ready so no other task can be entered and registering delayed resume'); |
242 | matrix.not_ready = true; | 245 | matrix.not_ready = true; |
243 | 246 | ||
244 | resumeHandlers['trx' + trx_id] = setTimeout(function() { | 247 | resumeHandlers['trx' + trx_id] = setTimeout(function() { |
245 | logger.verbose('Resuming supsended modem', {trx_id: trx_id}); | 248 | logger.verbose('Resuming supsended modem', {trx_id: trx_id}); |
246 | matrix.not_ready = false; | 249 | matrix.not_ready = false; |
247 | report({ | 250 | report({ |
248 | trx_id: trx_id, | 251 | trx_id: trx_id, |
249 | rc: '68', | 252 | rc: '68', |
250 | message: 'USSD timeout' | 253 | message: 'USSD timeout' |
251 | }) | 254 | }) |
252 | }, Number(config.partner.modem.suspend_time_ms) || 20 * 1000 ) | 255 | }, Number(config.partner.modem.suspend_time_ms) || 20 * 1000 ) |
253 | } | 256 | } |
254 | 257 | ||
255 | function deleteResumeHandler(trx_id) { | 258 | function deleteResumeHandler(trx_id) { |
256 | if (resumeHandlers['trx' + trx_id]) { | 259 | if (resumeHandlers['trx' + trx_id]) { |
257 | logger.verbose('Unregistering delayed resume of trx id ' + trx_id); | 260 | logger.verbose('Unregistering delayed resume of trx id ' + trx_id); |
258 | clearTimeout(resumeHandlers['trx' + trx_id]); | 261 | clearTimeout(resumeHandlers['trx' + trx_id]); |
259 | delete resumeHandlers['trx' + trx_id]; | 262 | delete resumeHandlers['trx' + trx_id]; |
260 | } | 263 | } |
261 | } | 264 | } |
262 | 265 | ||
263 | function onTrxFinish(trx_id) { | 266 | function onTrxFinish(trx_id) { |
264 | deleteResumeHandler(trx_id); | 267 | deleteResumeHandler(trx_id); |
265 | pendingArchive.remove(trx_id); | 268 | pendingArchive.remove(trx_id); |
266 | matrix.not_ready = false; | 269 | matrix.not_ready = false; |
267 | } | 270 | } |
268 | 271 | ||
269 | function buy(task) { | 272 | function buy(task) { |
270 | if (task.product === task.remote_product) { | 273 | if (task.product === task.remote_product) { |
271 | report({ | 274 | report({ |
272 | trx_id: task.trx_id, | 275 | trx_id: task.trx_id, |
273 | rc: '40', | 276 | rc: '40', |
274 | message: 'INTERNAL: Gagal melakukan transaksi. Kode USSD belum terdefinisi.' | 277 | message: 'INTERNAL: Gagal melakukan transaksi. Kode USSD belum terdefinisi.' |
275 | }); | 278 | }); |
276 | return; | 279 | return; |
277 | } | 280 | } |
278 | 281 | ||
279 | suspendPull(task.trx_id); | 282 | suspendPull(task.trx_id); |
280 | last_trx_id = task.trx_id; | 283 | last_trx_id = task.trx_id; |
281 | 284 | ||
282 | pendingArchive.put(task, function(err) { | 285 | pendingArchive.put(task, function(err) { |
283 | if (err) { | 286 | if (err) { |
284 | logger.verbose('Error inserting task to pending archive', {trx_id: task.trx_id, destination: task.destination, product: task.product, err: err}); | 287 | logger.verbose('Error inserting task to pending archive', {trx_id: task.trx_id, destination: task.destination, product: task.product, err: err}); |
285 | onTrxFinish(task.trx_id); | 288 | onTrxFinish(task.trx_id); |
286 | report({ | 289 | report({ |
287 | trx_id: task.trx_id, | 290 | trx_id: task.trx_id, |
288 | rc: '40', | 291 | rc: '40', |
289 | message: 'INTERNAL: Gagal melakukan transaksi. Mungkin ada transaksi dengan nomor dengan produk yang sama yang masih diproses. Silahkan ulangi beberapa saat lagi.' | 292 | message: 'INTERNAL: Gagal melakukan transaksi. Mungkin ada transaksi dengan nomor dengan produk yang sama yang masih diproses. Silahkan ulangi beberapa saat lagi.' |
290 | }); | 293 | }); |
291 | 294 | ||
292 | return; | 295 | return; |
293 | } | 296 | } |
294 | 297 | ||
295 | const ussd_command = task.remote_product.split(',')[0].replace("<DESTINATION>", task.destination).replace("<PIN>", config.partner.pin); | 298 | const ussd_command = task.remote_product.split(',')[0].replace("<DESTINATION>", task.destination).replace("<PIN>", config.partner.pin); |
296 | logger.verbose('Going to execute USSD', {trx_id: task.trx_id, destination: task.destination, product: task.product, ussd: ussd_command}); | 299 | logger.verbose('Going to execute USSD', {trx_id: task.trx_id, destination: task.destination, product: task.product, ussd: ussd_command}); |
297 | 300 | ||
298 | db.run("INSERT INTO ussd VALUES (?, ?, 'OUT', ?, ?)", moment().format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD'), matrix.modem.imsi, 'AT+CUSD=1,"' + ussd_command + '",15', function(err) { | 301 | db.run("INSERT INTO ussd VALUES (?, ?, 'OUT', ?, ?)", moment().format('YYYY-MM-DD HH:mm:ss'), moment().format('YYYY-MM-DD'), matrix.modem.imsi, 'AT+CUSD=1,"' + ussd_command + '",15', function(err) { |
299 | if (err) { | 302 | if (err) { |
300 | logger.warn('Error inserting ussd command to local database', {err: err}); | 303 | logger.warn('Error inserting ussd command to local database', {err: err}); |
301 | } | 304 | } |
302 | }); | 305 | }); |
303 | modem.sendUSSD(ussd_command, function() {}); | 306 | modem.sendUSSD(ussd_command, function() {}); |
304 | }) | 307 | }) |
305 | 308 | ||
306 | 309 | ||
307 | } | 310 | } |
308 | 311 | ||
309 | function report(data) { | 312 | function report(data) { |
310 | pullgw.report(data); | 313 | pullgw.report(data); |
311 | } | 314 | } |
312 | 315 | ||
313 | exports.buy = buy; | 316 | exports.buy = buy; |
314 | 317 |