Skip to content

Instantly share code, notes, and snippets.

@blisssan
Created August 24, 2021 19:58
Show Gist options
  • Save blisssan/c7f0f7f097ddce81172cd5f7f9f1a303 to your computer and use it in GitHub Desktop.
Save blisssan/c7f0f7f097ddce81172cd5f7f9f1a303 to your computer and use it in GitHub Desktop.
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