Commit 0cda3b29fd1f18b8c032e5b82da8c6c4cb594927

Authored by Adhidarma Hadiwinoto
1 parent 636896a548
Exists in master

PIN bisa dapat dari database

Showing 1 changed file with 20 additions and 3 deletions Inline Diff

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