Created
August 24, 2021 19:58
-
-
Save blisssan/c7f0f7f097ddce81172cd5f7f9f1a303 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'dart:convert'; | |
import 'dart:io'; | |
import 'package:charcode/ascii.dart'; | |
class PaymentTerminalService { | |
static var ascii = AsciiCodec(); | |
String ip = '127.0.0.1'; | |
int port = 4567; | |
// Instantiate the Service class with Ip & Port of the POS Terminal | |
PaymentTerminalService(String ip, String port) { | |
this.ip = ip; | |
this.port = int.tryParse(port) ?? 4567; | |
response = _controller.stream; | |
} | |
// There will be continuous packet exchanges between | |
// POS Terminal & the socket client, so its better to use | |
// streams so that the application can be reactive. | |
// but be sure to properly dispose the controller & resources | |
final StreamController<dynamic> _controller = StreamController.broadcast(); | |
Stream<dynamic> response; | |
Socket _socket; | |
// this is where the process of payment is initiated | |
// here the list of int obtained from the constructPaymentPacket is sent to the terminal | |
void makePayment(double amount) { | |
int amountInCents = (amount * 100).toInt(); | |
_socket?.add(constructPaymentPacket(amountInCents)); | |
} | |
// this has to be called before making payment | |
// it initiates a socket to the payment terminal | |
// the payment terminal is considered the server | |
// and our app is the client | |
Future<void> connect() async { | |
if (_socket != null) { | |
_socketClose(); | |
} | |
_socket = await Socket.connect(ip, port); | |
_socket.listen( | |
dataHandler, | |
onError: errorHandler, | |
onDone: doneHandler, | |
cancelOnError: false, | |
); | |
} | |
// this method transforms the amount entered | |
// to List<int> as prescribed by the payment terminal doc | |
// the values $0, $1, $fs are just hex representation of 0 1 & Field Separator | |
// the default way of doing that in dart is not great. | |
// therefore we went with charcode package | |
// the last line roughly translates like this 0x30 0x30 0x1C 0x30 0x30 0x31 .... | |
List<int> constructPaymentPacket(int amountInCents) { | |
List<int> paymentRequest = [$0, $0]; | |
List<int> amountTag = [$0, $0, $1]; | |
List<int> amountField = ascii.encode(amountInCents.toString()); | |
return [...paymentRequest, $fs, ...amountTag, ...amountField]; | |
} | |
// this method acts as the socket data listener | |
// when packets are sent by the terminal the response has to be parsed | |
// in an understandable way for our processing. | |
// our terminal every few seconds sends an heartbeat signal | |
// we can use this to know if the connection is alive. | |
// if the socket receive any other data apart from heartbeat | |
// we try to parse it to match our use cases | |
// this is done by the parseResponseData | |
// once parsed we turn the response into a DTO object | |
// to be shared with other parts of the application | |
// through the stream | |
void dataHandler(data) { | |
if (!(data is List<int>)) { | |
return; | |
} | |
if (data.length == 1 && data[0] == $dc1) { | |
print('Heart Beat'); | |
return; | |
} | |
List<String> responseData = _parseResponseData(data); | |
if (responseData.isEmpty) { | |
print('Empty Response'); | |
return; | |
} | |
_processResponseData(responseData); | |
} | |
// here we convert the List<int> back to string | |
// but here the parsing looks for $fs field separator byte | |
// this way we can know which part of the string represents | |
// status, which part represents additional info, etc | |
List<String> _parseResponseData(List<int> data) { | |
List<int> _temp = []; | |
List<String> responseData = []; | |
data.forEach((item) { | |
if (item == $fs) { | |
responseData.add(String.fromCharCodes(_temp)); | |
_temp = []; | |
} else { | |
_temp.add(item); | |
} | |
}); | |
if (_temp.isNotEmpty) { | |
responseData.add(String.fromCharCodes(_temp)); | |
} | |
return responseData; | |
} | |
// Exctract only the required information from the parsed data | |
// the above parse method usually will be same for all | |
// because most of the serial communication & tcp ip communcation | |
// uses a field separator byte to distinguish various segments of the | |
// data. But the process response data is upto you | |
// here we took only the first part of the response based on our terminal specification | |
// we took only first two characters of the first field since thats enough for our app | |
// to know about the payment request's status | |
void _processResponseData(List<String> responseData) { | |
print(responseData); | |
String _statusCode = responseData[0]; | |
_statusCode = _statusCode.substring(0, 2); | |
var response = | |
TerminalResponse.fromTransactionStatus(_statusCode, responseData); | |
_controller.sink.add(response); | |
} | |
void close() { | |
_socketClose(); | |
_controller.close(); | |
} | |
void _socketClose() { | |
_socket?.destroy(); | |
_socket == null; | |
} | |
// handle errors from socket response | |
void errorHandler(error, StackTrace trace) { | |
print(error); | |
close(); | |
} | |
// this is called when the connection is disconnected | |
void doneHandler() { | |
print('done'); | |
close(); | |
} | |
} | |
enum TerminalResponseStatus { success, failed, unknown } | |
// This is a custom class that we use to format the response | |
// to be used by other parts of the application | |
class TerminalResponse { | |
// these codes are specific to our terminal | |
// this might differ terminal to terminal | |
// this is the code that we extract in processResponse method | |
static Map<String, String> transactionStatuses = { | |
'00': 'Approved!', | |
'01': 'Partial Approved!', | |
'10': 'Declined by host or by card!', | |
'11': 'Communication Error!', | |
'12': 'Cancelled by User', | |
'13': 'Timed out on User Input', | |
'14': 'Transaction/Function Not Completed', | |
'15': 'Batch Empty', | |
'16': 'Declined by Merchant', | |
'17': 'Record Not Found', | |
'18': 'Transaction Already Voided', | |
'30': 'Invalid ECR Parameter', | |
'31': 'Battery low', | |
}; | |
TerminalResponseStatus status = TerminalResponseStatus.unknown; | |
dynamic data; | |
String code = ''; | |
String message = ''; | |
@override | |
String toString() { | |
return toMap().toString(); | |
} | |
Map<String, dynamic> toMap() { | |
return {'code': code, 'message': message, 'data': data}; | |
} | |
static TerminalResponse fromTransactionStatus(String code, data) { | |
String _statusMessage = transactionStatuses[code]; | |
if (_statusMessage == null) { | |
return TerminalResponse.unknown(code, 'Unknown', data); | |
} | |
if (code == '00') { | |
return TerminalResponse.success(code, _statusMessage, data); | |
} | |
return TerminalResponse.failure(code, _statusMessage, data); | |
} | |
TerminalResponse.success(code, String message, dynamic data) { | |
this.code = code; | |
this.message = message; | |
status = TerminalResponseStatus.success; | |
this.data = data; | |
} | |
TerminalResponse.failure(code, String message, dynamic data) { | |
this.code = code; | |
this.message = message; | |
status = TerminalResponseStatus.failed; | |
this.data = data; | |
} | |
TerminalResponse.unknown(code, String message, dynamic data) { | |
this.code = code; | |
this.message = message; | |
status = TerminalResponseStatus.unknown; | |
this.data = data; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment