@mx51/spi-client-js
Advanced tools
Comparing version 2.0.4 to 2.1.0
@@ -22,8 +22,14 @@ module.exports = function (config) { | ||
'src/**/*.js', | ||
'Pos.js', | ||
// Include the unit tests | ||
'tests/**/*.spec.js' | ||
'tests/**/*.spec.js', | ||
// Mock fixtures | ||
'./tests/fixtures/**/*.json' | ||
], | ||
// plugins: [ | ||
// 'karma-json-fixtures-preprocessor' | ||
// ], | ||
// list of files to exclude | ||
@@ -36,3 +42,8 @@ exclude: [ | ||
preprocessors: { | ||
'./tests/fixtures/**/*.json': ['json_fixtures'] | ||
}, | ||
jsonFixturesPreprocessor: { | ||
// strip this from the file path \ fixture name | ||
stripPrefix: 'tests/fixtures/' | ||
}, | ||
@@ -39,0 +50,0 @@ |
{ | ||
"name": "@mx51/spi-client-js", | ||
"version": "2.0.4", | ||
"version": "2.1.0", | ||
"description": "JavaScript Client Library for Assembly Payments' In-Store Integration.", | ||
@@ -8,3 +8,4 @@ "main": "karma.conf.js", | ||
"test": "karma start", | ||
"transpile": "babel src --out-file ./dist/spi-client-js.js" | ||
"transpile": "babel src --out-file ./dist/spi-client-js.js", | ||
"build": "babel src --out-file ./dist/spi-client-js.js" | ||
}, | ||
@@ -28,5 +29,6 @@ "repository": { | ||
"jasmine": "^2.8.0", | ||
"karma": "^1.7.1", | ||
"karma": "^2.0.2", | ||
"karma-chrome-launcher": "^2.2.0", | ||
"karma-jasmine": "^1.1.1" | ||
"karma-jasmine": "^1.1.1", | ||
"karma-json-fixtures-preprocessor": "0.0.6" | ||
}, | ||
@@ -33,0 +35,0 @@ "bugs": { |
@@ -7,4 +7,18 @@ const ConnectionState = { | ||
const SPI_PROTOCOL = 'spi.2.0.0'; | ||
const SPI_PROTOCOL = 'spi.2.1.0'; | ||
class ConnectionStateEventArgs | ||
{ | ||
constructor(connectionState) { | ||
this.ConnectionState = connectionState; | ||
} | ||
} | ||
class MessageEventArgs | ||
{ | ||
constructor(message) { | ||
this.Message = message; | ||
} | ||
} | ||
class Connection { | ||
@@ -39,3 +53,3 @@ constructor() { | ||
document.dispatchEvent(new CustomEvent('ConnectionStatusChanged', {detail: this.State})); | ||
document.dispatchEvent(new CustomEvent('ConnectionStatusChanged', {detail: new ConnectionStateEventArgs(ConnectionState.Connecting)})); | ||
} | ||
@@ -67,3 +81,3 @@ | ||
this.Connected = true; | ||
document.dispatchEvent(new CustomEvent('ConnectionStatusChanged', {detail: this.State})); | ||
document.dispatchEvent(new CustomEvent('ConnectionStatusChanged', {detail: new ConnectionStateEventArgs(ConnectionState.Connected)})); | ||
} | ||
@@ -75,3 +89,3 @@ | ||
this._ws = null; | ||
document.dispatchEvent(new CustomEvent('ConnectionStatusChanged', {detail: this.State})); | ||
document.dispatchEvent(new CustomEvent('ConnectionStatusChanged', {detail: new ConnectionStateEventArgs(ConnectionState.Disconnected)})); | ||
} | ||
@@ -94,8 +108,8 @@ | ||
onMessageReceived(message) { | ||
document.dispatchEvent(new CustomEvent('MessageReceived', {detail: message})); | ||
document.dispatchEvent(new CustomEvent('MessageReceived', {detail: new MessageEventArgs(message.data)})); | ||
} | ||
onError(err) { | ||
document.dispatchEvent(new CustomEvent('ErrorReceived', {detail: err})); | ||
document.dispatchEvent(new CustomEvent('ErrorReceived', {detail: new MessageEventArgs(err)})); | ||
} | ||
} |
@@ -13,8 +13,8 @@ class Crypto { | ||
/// <summary> | ||
/// Encrypt a block using CBC and PKCS7. | ||
/// </summary> | ||
/// <param name="key">The key value</param> | ||
/// <param name="data">The message to encrypt</param> | ||
/// <returns>Returns the resulting encrypted string data as HEX.</returns> | ||
// <summary> | ||
// Encrypt a block using CBC and PKCS7. | ||
// </summary> | ||
// <param name="key">The key value</param> | ||
// <param name="data">The message to encrypt</param> | ||
// <returns>Returns the resulting encrypted string data as HEX.</returns> | ||
static AesEncrypt (key, data) { | ||
@@ -31,8 +31,8 @@ let bytes = aesjs.utils.hex.toBytes(key); | ||
/// <summary> | ||
/// Decrypt a block using a CBC and PKCS7. | ||
/// </summary> | ||
/// <param name="key">The key value</param> | ||
/// <param name="data">the data to decrypt</param> | ||
/// <returns>Returns the resulting data decrypted in plaintext.</returns> | ||
// <summary> | ||
// Decrypt a block using a CBC and PKCS7. | ||
// </summary> | ||
// <param name="key">The key value</param> | ||
// <param name="data">the data to decrypt</param> | ||
// <returns>Returns the resulting data decrypted in plaintext.</returns> | ||
static AesDecrypt(key, data) { | ||
@@ -49,8 +49,8 @@ let bytes = aesjs.utils.hex.toBytes(key); | ||
/// <summary> | ||
/// Calculates the HMACSHA256 signature of a message. | ||
/// </summary> | ||
/// <param name="key">The Hmac Key as HEX</param> | ||
/// <param name="messageToSign">The message to sign</param> | ||
/// <returns>The HMACSHA256 signature as a hex string</returns> | ||
// <summary> | ||
// Calculates the HMACSHA256 signature of a message. | ||
// </summary> | ||
// <param name="key">The Hmac Key as HEX</param> | ||
// <param name="messageToSign">The message to sign</param> | ||
// <returns>The HMACSHA256 signature as a hex string</returns> | ||
static HmacSignature(key, messageToSign) { | ||
@@ -57,0 +57,0 @@ let shaObj = new jsSHA("SHA-256", "TEXT"); |
@@ -5,10 +5,10 @@ // This creates the private and public keys for diffie-hellman (https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange#Cryptographic_explanation) | ||
/// <summary> | ||
/// This class implements the Diffie-Hellman algorithm using BigIntegers. | ||
/// It can do the 3 main things: | ||
/// 1. Generate a random Private Key for you. | ||
/// 2. Generate your Public Key based on your Private Key. | ||
/// 3. Generate the Secret given their Public Key and your Private Key | ||
/// p and g are the shared constants for the algorithm, aka primeP and primeG. | ||
/// </summary> | ||
// <summary> | ||
// This class implements the Diffie-Hellman algorithm using BigIntegers. | ||
// It can do the 3 main things: | ||
// 1. Generate a random Private Key for you. | ||
// 2. Generate your Public Key based on your Private Key. | ||
// 3. Generate the Secret given their Public Key and your Private Key | ||
// p and g are the shared constants for the algorithm, aka primeP and primeG. | ||
// </summary> | ||
class DiffieHellman { | ||
@@ -26,7 +26,7 @@ | ||
/// <summary> | ||
/// Generates a random Private Key that you can use. | ||
/// </summary> | ||
/// <param name="p"></param> | ||
/// <returns>Random Private Key</returns> | ||
// <summary> | ||
// Generates a random Private Key that you can use. | ||
// </summary> | ||
// <param name="p"></param> | ||
// <returns>Random Private Key</returns> | ||
RandomPrivateKey(maxValue) { | ||
@@ -45,9 +45,9 @@ let maxValueBN = new BN(maxValue); | ||
/// <summary> | ||
/// Calculates the Public Key from a Private Key. | ||
/// </summary> | ||
/// <param name="p"></param> | ||
/// <param name="g"></param> | ||
/// <param name="privateKey"></param> | ||
/// <returns>Public Key (Hex)</returns> | ||
// <summary> | ||
// Calculates the Public Key from a Private Key. | ||
// </summary> | ||
// <param name="p"></param> | ||
// <param name="g"></param> | ||
// <param name="privateKey"></param> | ||
// <returns>Public Key (Hex)</returns> | ||
PublicKey(p, g, privateKey) { | ||
@@ -63,9 +63,9 @@ const aHex = new BN(privateKey, 16); | ||
/// <summary> | ||
/// Calculates the shared secret given their Public Key (A) and your Private Key (b) | ||
/// </summary> | ||
/// <param name="p"></param> | ||
/// <param name="theirPublicKey"></param> | ||
/// <param name="yourPrivateKey"></param> | ||
/// <returns></returns> | ||
// <summary> | ||
// Calculates the shared secret given their Public Key (A) and your Private Key (b) | ||
// </summary> | ||
// <param name="p"></param> | ||
// <param name="theirPublicKey"></param> | ||
// <param name="yourPrivateKey"></param> | ||
// <returns></returns> | ||
Secret(p, theirPublicKey, yourPrivateKey) { | ||
@@ -72,0 +72,0 @@ const bHex = new BN(theirPublicKey, 16); |
@@ -1,4 +0,4 @@ | ||
/// <summary> | ||
/// Events statically declares the various event names in messages. | ||
/// </summary> | ||
// <summary> | ||
// Events statically declares the various event names in messages. | ||
// </summary> | ||
const Events = { | ||
@@ -10,2 +10,3 @@ PairRequest : "pair_request", | ||
PairResponse : "pair_response", | ||
DropKeysAdvice : "drop_keys", | ||
@@ -28,6 +29,16 @@ LoginRequest : "login_request", | ||
SignatureAccepted : "signature_accept", | ||
AuthCodeRequired : "authorisation_code_required", | ||
AuthCodeAdvice : "authorisation_code_advice", | ||
CashoutOnlyRequest : "cash", | ||
CashoutOnlyResponse : "cash_response", | ||
MotoPurchaseRequest : "moto_purchase", | ||
MotoPurchaseResponse : "moto_purchase_response", | ||
SettleRequest : "settle", | ||
SettleResponse : "settle_response", | ||
SettlementEnquiryRequest : "settlement_enquiry", | ||
SettlementEnquiryResponse : "settlement_enquiry_response", | ||
KeyRollRequest : "request_use_next_keys", | ||
@@ -38,3 +49,10 @@ KeyRollResponse : "response_use_next_keys", | ||
InvalidHmacSignature : "_INVALID_SIGNATURE_" | ||
InvalidHmacSignature : "_INVALID_SIGNATURE_", | ||
// Pay At Table Related Messages | ||
PayAtTableGetTableConfig : "get_table_config", // incoming. When eftpos wants to ask us for P@T configuration. | ||
PayAtTableSetTableConfig : "set_table_config", // outgoing. When we want to instruct eftpos with the P@T configuration. | ||
PayAtTableGetBillDetails : "get_bill_details", // incoming. When eftpos wants to aretrieve the bill for a table. | ||
PayAtTableBillDetails : "bill_details", // outgoing. We reply with this when eftpos requests to us get_bill_details. | ||
PayAtTableBillPayment : "bill_payment" // incoming. When the eftpos advices | ||
}; | ||
@@ -46,6 +64,6 @@ | ||
/// <summary> | ||
/// MessageStamp represents what is required to turn an outgoing Message into Json | ||
/// including encryption and date setting. | ||
/// </summary> | ||
// <summary> | ||
// MessageStamp represents what is required to turn an outgoing Message into Json | ||
// including encryption and date setting. | ||
// </summary> | ||
class MessageStamp { | ||
@@ -59,30 +77,30 @@ constructor(posId, secrets, serverTimeDelta) { | ||
/// <summary> | ||
/// MessageEnvelope represents the outer structure of any message that is exchanged | ||
/// between the Pos and the PinPad and vice-versa. | ||
/// See http://www.simplepaymentapi.com/#/api/message-encryption | ||
/// </summary> | ||
// <summary> | ||
// MessageEnvelope represents the outer structure of any message that is exchanged | ||
// between the Pos and the PinPad and vice-versa. | ||
// See http://www.simplepaymentapi.com/#/api/message-encryption | ||
// </summary> | ||
class MessageEnvelope { | ||
constructor(message, enc, hmac, posId) { | ||
/// <summary> | ||
/// The Message field is set only when in Un-encrypted form. | ||
/// In fact it is the only field in an envelope in the Un-Encrypted form. | ||
/// </summary> | ||
// <summary> | ||
// The Message field is set only when in Un-encrypted form. | ||
// In fact it is the only field in an envelope in the Un-Encrypted form. | ||
// </summary> | ||
this.Message = message; | ||
/// <summary> | ||
/// The enc field is set only when in Encrypted form. | ||
/// It contains the encrypted Json of another MessageEnvelope | ||
/// </summary> | ||
// <summary> | ||
// The enc field is set only when in Encrypted form. | ||
// It contains the encrypted Json of another MessageEnvelope | ||
// </summary> | ||
this.Enc = enc; | ||
/// <summary> | ||
/// The hmac field is set only when in Encrypted form. | ||
/// It is the signature of the "enc" field. | ||
/// </summary> | ||
// <summary> | ||
// The hmac field is set only when in Encrypted form. | ||
// It is the signature of the "enc" field. | ||
// </summary> | ||
this.Hmac = hmac; | ||
/// <summary> | ||
/// The pos_id field is only filled for outgoing Encrypted messages. | ||
/// </summary> | ||
// <summary> | ||
// The pos_id field is only filled for outgoing Encrypted messages. | ||
// </summary> | ||
this.PosId = posId; | ||
@@ -101,6 +119,6 @@ } | ||
/// <summary> | ||
/// Message represents the contents of a Message. | ||
/// See http://www.simplepaymentapi.com/#/api/message-encryption | ||
/// </summary> | ||
// <summary> | ||
// Message represents the contents of a Message. | ||
// See http://www.simplepaymentapi.com/#/api/message-encryption | ||
// </summary> | ||
class Message { | ||
@@ -127,5 +145,9 @@ constructor(id, eventName, data, needsEncryption) { | ||
GetError() { | ||
return this.Data.error_reason ? this.Data.error_reason : "NONE"; | ||
return this.Data.error_reason ? this.Data.error_reason : ""; | ||
} | ||
GetErrorDetail() { | ||
return this.Data.error_detail; | ||
} | ||
GetServerTimeDelta() | ||
@@ -147,2 +169,14 @@ { | ||
// Helper method to parse bank date format 20042018 (ddMMyyyy) | ||
static ParseBankDate(bankDate) { | ||
if(bankDate.length !== 8) return null; | ||
return new Date(`${bankDate.substr(4,4)}-${bankDate.substr(2,2)}-${bankDate.substr(0,2)}`); | ||
} | ||
// Parses a bank date & time str from "05Oct17" / "05:00" ("ddMMMyy/HH:mm") into date obj | ||
static ParseBankDateTimeStr(date, time) { | ||
return new Date(`${date.substr(0,2)} ${date.substr(2,3)} ${date.substr(5,2)} ${time}`); | ||
} | ||
static FromJson(msgJson, secrets) { | ||
@@ -157,2 +191,9 @@ let env = JSON.parse(msgJson); | ||
if (secrets == null) | ||
{ | ||
// This may happen if we somehow received an encrypted message from eftpos but we're not configered with secrets. | ||
// For example, if we cancel the pairing process a little late in the game and we get an encrypted key_check message after we've dropped the keys. | ||
return new Message("UNKNOWN", "NOSECRETS", null, false); | ||
} | ||
// Its encrypted, verify sig | ||
@@ -179,3 +220,3 @@ let sig = Crypto.HmacSignature(secrets.HmacKey, env.enc); | ||
} catch(e) { | ||
return new Message("Unknown", "unparseable", {"msg": decryptedJson}, false); | ||
return new Message("UNKNOWN", "UNPARSEABLE", {"msg": decryptedJson}, false); | ||
} | ||
@@ -182,0 +223,0 @@ } |
@@ -1,4 +0,4 @@ | ||
/// <summary> | ||
/// Pairing Interaction 1: Outgoing | ||
/// </summary> | ||
// <summary> | ||
// Pairing Interaction 1: Outgoing | ||
// </summary> | ||
class PairRequest { | ||
@@ -11,3 +11,3 @@ ToMessage() { | ||
/// Pairing Interaction 2: Incoming | ||
// Pairing Interaction 2: Incoming | ||
class KeyRequest { | ||
@@ -21,3 +21,3 @@ constructor(m) { | ||
/// Pairing Interaction 3: Outgoing | ||
// Pairing Interaction 3: Outgoing | ||
class KeyResponse { | ||
@@ -44,3 +44,3 @@ constructor(requestId, Benc, Bhmac) { | ||
/// Pairing Interaction 4: Incoming | ||
// Pairing Interaction 4: Incoming | ||
class KeyCheck { | ||
@@ -52,3 +52,3 @@ constructor(m) { | ||
/// Pairing Interaction 5: Incoming | ||
// Pairing Interaction 5: Incoming | ||
class PairResponse { | ||
@@ -60,3 +60,3 @@ constructor(m) { | ||
/// Holder class for Secrets and KeyResponse, so that we can use them together in method signatures. | ||
// Holder class for Secrets and KeyResponse, so that we can use them together in method signatures. | ||
class SecretsAndKeyResponse { | ||
@@ -68,1 +68,9 @@ constructor(secrets, keyResponse) { | ||
} | ||
class DropKeysRequest | ||
{ | ||
ToMessage() | ||
{ | ||
return new Message(RequestIdHelper.Id("drpkys"), Events.DropKeysAdvice, null, true); | ||
} | ||
} |
@@ -7,11 +7,11 @@ // This is the generator used for diffie-hellman in 2048-bit MODP Group 14 as per (https://tools.ietf.org/html/rfc3526#section-3) | ||
/// <summary> | ||
/// This static class helps you with the pairing process as documented here: | ||
/// http://www.simplepaymentapi.com/#/api/pairing-process | ||
/// </summary> | ||
// <summary> | ||
// This static class helps you with the pairing process as documented here: | ||
// http://www.simplepaymentapi.com/#/api/pairing-process | ||
// </summary> | ||
class PairingHelper { | ||
/// <summary> | ||
/// Generates a pairing Request. | ||
/// </summary> | ||
/// <returns>New PairRequest</returns> | ||
// <summary> | ||
// Generates a pairing Request. | ||
// </summary> | ||
// <returns>New PairRequest</returns> | ||
static NewPairRequest() { | ||
@@ -21,7 +21,7 @@ return new PairRequest(); | ||
/// <summary> | ||
/// Calculates/Generates Secrets and KeyResponse given an incoming KeyRequest. | ||
/// </summary> | ||
/// <param name="keyRequest"></param> | ||
/// <returns>Secrets and KeyResponse to send back.</returns> | ||
// <summary> | ||
// Calculates/Generates Secrets and KeyResponse given an incoming KeyRequest. | ||
// </summary> | ||
// <param name="keyRequest"></param> | ||
// <returns>Secrets and KeyResponse to send back.</returns> | ||
GenerateSecretsAndKeyResponse(keyRequest) { | ||
@@ -42,8 +42,8 @@ let encPubAndSec = this._calculateMyPublicKeyAndSecret(keyRequest.Aenc); | ||
/// <summary> | ||
/// Turns an incoming "A" value from the PinPad into the outgoing "B" value | ||
/// and the secret value using DiffieHelmman helper. | ||
/// </summary> | ||
/// <param name="theirPublicKey">The incoming A value</param> | ||
/// <returns>Your B value and the Secret</returns> | ||
// <summary> | ||
// Turns an incoming "A" value from the PinPad into the outgoing "B" value | ||
// and the secret value using DiffieHelmman helper. | ||
// </summary> | ||
// <param name="theirPublicKey">The incoming A value</param> | ||
// <returns>Your B value and the Secret</returns> | ||
_calculateMyPublicKeyAndSecret(theirPublicKey) { | ||
@@ -61,9 +61,9 @@ | ||
/// <summary> | ||
/// Converts the DH secret BigInteger into the hex-string to be used as the secret. | ||
/// There are some "gotchyas" here which is why this piece of work is abstracted so it can be tested separately. | ||
/// See: http://www.simplepaymentapi.com/#/api/pairing-process | ||
/// </summary> | ||
/// <param name="secretBI">Secret as BigInteger</param> | ||
/// <returns>Secret as Hex-String</returns> | ||
// <summary> | ||
// Converts the DH secret BigInteger into the hex-string to be used as the secret. | ||
// There are some "gotchyas" here which is why this piece of work is abstracted so it can be tested separately. | ||
// See: http://www.simplepaymentapi.com/#/api/pairing-process | ||
// </summary> | ||
// <param name="secretBI">Secret as BigInteger</param> | ||
// <returns>Secret as Hex-String</returns> | ||
DHSecretToSPISecret(secret) | ||
@@ -83,5 +83,5 @@ { | ||
/// <summary> | ||
/// Internal Holder class for Public and Secret, so that we can use them together in method signatures. | ||
/// </summary> | ||
// <summary> | ||
// Internal Holder class for Public and Secret, so that we can use them together in method signatures. | ||
// </summary> | ||
class PublicKeyAndSecret { | ||
@@ -88,0 +88,0 @@ constructor(myPublicKey, sharedSecretKey) { |
class PurchaseRequest { | ||
constructor(amountCents, id) { | ||
constructor(amountCents, posRefId) { | ||
this.PosRefId = posRefId; | ||
this.PurchaseAmount = amountCents; | ||
this.TipAmount = 0; | ||
this.CashoutAmount = 0; | ||
this.PromptForCashout = false; | ||
this.Config = new SpiConfig(); | ||
// Library Backwards Compatibility | ||
this.Id = posRefId; | ||
this.AmountCents = amountCents; | ||
this.Id = id; | ||
} | ||
AmountSummary() | ||
{ | ||
return `Purchase: ${(this.PurchaseAmount / 100.0).toFixed(2)}; | ||
Tip: ${(this.TipAmount / 100.0).toFixed(2)}; | ||
Cashout: ${(this.CashoutAmount / 100.0).toFixed(2)};`; | ||
} | ||
ToMessage() { | ||
let data = { | ||
amount_purchase: this.AmountCents | ||
pos_ref_id: this.PosRefId, | ||
purchase_amount: this.PurchaseAmount, | ||
tip_amount: this.TipAmount, | ||
cash_amount: this.CashoutAmount, | ||
prompt_for_cashout: this.PromptForCashout | ||
}; | ||
return new Message(this.Id, Events.PurchaseRequest, data, true); | ||
this.Config.addReceiptConfig(data); | ||
return new Message(RequestIdHelper.Id("prchs"), Events.PurchaseRequest, data, true); | ||
} | ||
@@ -20,5 +40,7 @@ } | ||
{ | ||
this._m = m; | ||
this.RequestId = m.Id; | ||
this._m = m; | ||
this.PosRefId = m.Data.pos_ref_id; | ||
this.SchemeName = m.Data.scheme_name; | ||
this.SchemeAppName = m.Data.scheme_name; | ||
this.Success = m.GetSuccessState() == SuccessState.Success; | ||
@@ -32,2 +54,27 @@ } | ||
GetPurchaseAmount() | ||
{ | ||
return this._m.Data.purchase_amount; | ||
} | ||
GetTipAmount() | ||
{ | ||
return this._m.Data.tip_amount; | ||
} | ||
GetCashoutAmount() | ||
{ | ||
return this._m.Data.cash_amount; | ||
} | ||
GetBankNonCashAmount() | ||
{ | ||
return this._m.Data.bank_noncash_amount; | ||
} | ||
GetBankCashAmount() | ||
{ | ||
return this._m.Data.bank_cash_amount; | ||
} | ||
GetCustomerReceipt() | ||
@@ -48,2 +95,65 @@ { | ||
GetResponseCode() | ||
{ | ||
return this._m.Data.host_response_code; | ||
} | ||
GetTerminalReferenceId() | ||
{ | ||
return this._m.Data.terminal_ref_id; | ||
} | ||
GetCardEntry() | ||
{ | ||
return this._m.Data.card_entry; | ||
} | ||
GetAccountType() | ||
{ | ||
return this._m.Data.account_type; | ||
} | ||
GetAuthCode() | ||
{ | ||
return this._m.Data.auth_code; | ||
} | ||
GetBankDate() | ||
{ | ||
return this._m.Data.bank_date; | ||
} | ||
GetBankTime() | ||
{ | ||
return this._m.Data.bank_time; | ||
} | ||
GetMaskedPan() | ||
{ | ||
return this._m.Data.masked_pan; | ||
} | ||
GetTerminalId() | ||
{ | ||
return this._m.Data.terminal_id; | ||
} | ||
WasMerchantReceiptPrinted() | ||
{ | ||
return this._m.Data.merchant_receipt_printed; | ||
} | ||
WasCustomerReceiptPrinted() | ||
{ | ||
return this._m.Data.customer_receipt_printed; | ||
} | ||
GetSettlementDate() | ||
{ | ||
//"bank_settlement_date":"20042018" | ||
var dateStr = this._m.Data.bank_settlement_date; | ||
if (!dateStr) return null; | ||
return Message.ParseBankDate(dateStr); | ||
} | ||
GetResponseValue(attribute) | ||
@@ -53,2 +163,21 @@ { | ||
} | ||
ToPaymentSummary() | ||
{ | ||
return { | ||
account_type: this.GetAccountType(), | ||
auth_code: this.GetAuthCode(), | ||
bank_date: this.GetBankDate(), | ||
bank_time: this.GetBankTime(), | ||
host_response_code: this.GetResponseCode(), | ||
host_response_text: this.GetResponseText(), | ||
masked_pan: this.GetMaskedPan(), | ||
purchase_amount: this.GetPurchaseAmount(), | ||
rrn: this.GetRRN(), | ||
scheme_name: this.SchemeName, | ||
terminal_id: this.GetTerminalId(), | ||
terminal_ref_id: this.GetTerminalReferenceId(), | ||
tip_amount: this.GetTipAmount() | ||
}; | ||
} | ||
} | ||
@@ -67,5 +196,2 @@ | ||
{ | ||
constructor(id) { | ||
this.Id = id; | ||
} | ||
ToMessage() | ||
@@ -82,3 +208,2 @@ { | ||
this._m = m; | ||
this.Success = (m.GetSuccessState() === SuccessState.Success); | ||
} | ||
@@ -90,4 +215,5 @@ | ||
// as retrieval may be successful, but the retrieved transaction was a fail. | ||
// So we check if we got back an RRN. | ||
return !!this.GetRRN(); | ||
// So we check if we got back an ResponseCode. | ||
// (as opposed to say an operation_in_progress_error) | ||
return !!this.GetResponseCode(); | ||
} | ||
@@ -97,17 +223,25 @@ | ||
{ | ||
return this._m.GetError() == "OPERATION_IN_PROGRESS"; | ||
return this._m.GetError().startsWith("OPERATION_IN_PROGRESS"); | ||
} | ||
GetSuccessState() | ||
IsWaitingForSignatureResponse() | ||
{ | ||
return this._m.GetSuccessState(); | ||
return this._m.GetError().startsWith("OPERATION_IN_PROGRESS_AWAITING_SIGNATURE"); | ||
} | ||
GetResponseText() | ||
IsWaitingForAuthCode() | ||
{ | ||
return this._m.Data.host_response_text | ""; | ||
return this._m.GetError().startsWith("OPERATION_IN_PROGRESS_AWAITING_PHONE_AUTH_CODE"); | ||
} | ||
GetMerchantReceipt() | ||
IsStillInProgress(posRefId) | ||
{ | ||
return this._m.Data.merchant_receipt || ""; | ||
return this.WasOperationInProgressError() && this.GetPosRefId() == posRefId; | ||
} | ||
GetSuccessState() | ||
{ | ||
return this._m.GetSuccessState(); | ||
} | ||
WasSuccessfulTx() | ||
@@ -123,2 +257,7 @@ { | ||
GetPosRefId() | ||
{ | ||
return this._m.Data.pos_ref_id; | ||
} | ||
GetSchemeApp() | ||
@@ -128,3 +267,8 @@ { | ||
} | ||
GetSchemeName() | ||
{ | ||
return this._m.Data.scheme_name; | ||
} | ||
GetAmount() | ||
@@ -151,14 +295,19 @@ { | ||
GetResponseValue(attribute) | ||
GetResponseText() | ||
{ | ||
return this._m.Data[attribute]; | ||
} | ||
return this._m.Data.host_response_text | ""; | ||
} | ||
/// <summary> | ||
/// There is a bug, VSV-920, whereby the customer_receipt is missing from a glt response. | ||
/// The current recommendation is to use the merchant receipt in place of it if required. | ||
/// This method modifies the underlying incoming message data by copying | ||
/// the merchant receipt into the customer receipt only if there | ||
/// is a merchant_receipt and there is not a customer_receipt. | ||
/// </summary> | ||
GetResponseCode() | ||
{ | ||
return this._m.Data.host_response_code; | ||
} | ||
// <summary> | ||
// There is a bug, VSV-920, whereby the customer_receipt is missing from a glt response. | ||
// The current recommendation is to use the merchant receipt in place of it if required. | ||
// This method modifies the underlying incoming message data by copying | ||
// the merchant receipt into the customer receipt only if there | ||
// is a merchant_receipt and there is not a customer_receipt. | ||
// </summary> | ||
CopyMerchantReceiptToCustomerReceipt() | ||
@@ -177,6 +326,8 @@ { | ||
{ | ||
constructor(amountCents, id) | ||
constructor(amountCents, posRefId) | ||
{ | ||
this.AmountCents = amountCents; | ||
this.Id = id; | ||
this.Id = RequestIdHelper.Id("refund"); | ||
this.PosRefId = posRefId; | ||
this.Config = new SpiConfig(); | ||
} | ||
@@ -186,6 +337,6 @@ | ||
{ | ||
let data = {amount_purchase: this.AmountCents}; | ||
return new Message(this.Id, Events.RefundRequest, data, true); | ||
let data = {refund_amount: this.AmountCents, pos_ref_id: this.PosRefId}; | ||
this.Config.addReceiptConfig(data); | ||
return new Message(RequestIdHelper.Id("refund"), Events.RefundRequest, data, true); | ||
} | ||
} | ||
@@ -197,8 +348,15 @@ | ||
{ | ||
this._m = m; | ||
this.RequestId = m.Id; | ||
this._m = m; | ||
this.PosRefId = m.Data.pos_ref_id; | ||
this.SchemeName = m.Data.scheme_name; | ||
this.SchemeAppName = m.Data.scheme_name; | ||
this.Success = m.GetSuccessState() == SuccessState.Success; | ||
} | ||
GetRefundAmount() | ||
{ | ||
return this._m.Data.refund_amount; | ||
} | ||
GetRRN() | ||
@@ -218,3 +376,3 @@ { | ||
} | ||
GetResponseText() | ||
@@ -224,3 +382,57 @@ { | ||
} | ||
GetResponseCode() | ||
{ | ||
return this._m.Data.host_response_code || ""; | ||
} | ||
GetTerminalReferenceId() | ||
{ | ||
return this._m.Data.terminal_ref_id || ""; | ||
} | ||
GetCardEntry() | ||
{ | ||
return this._m.Data.card_entry || ""; | ||
} | ||
GetAccountType() | ||
{ | ||
return this._m.Data.account_type || ""; | ||
} | ||
GetAuthCode() | ||
{ | ||
return this._m.Data.auth_code || ""; | ||
} | ||
GetBankDate() | ||
{ | ||
return this._m.Data.bank_date || ""; | ||
} | ||
GetBankTime() | ||
{ | ||
return this._m.Data.bank_time || ""; | ||
} | ||
GetMaskedPan() | ||
{ | ||
return this._m.Data.masked_pan || ""; | ||
} | ||
GetTerminalId() | ||
{ | ||
return this._m.Data.terminal_id || ""; | ||
} | ||
WasMerchantReceiptPrinted() | ||
{ | ||
return this._m.Data.merchant_receipt_printed; | ||
} | ||
WasCustomerReceiptPrinted() | ||
{ | ||
return this._m.Data.customer_receipt_printed; | ||
} | ||
GetSettlementDate() | ||
{ | ||
//"bank_settlement_date":"20042018" | ||
var dateStr = this._m.Data.bank_settlement_date; | ||
if (!dateStr) return null; | ||
return Message.ParseBankDate(dateStr); | ||
} | ||
GetResponseValue(attribute) | ||
@@ -237,8 +449,16 @@ { | ||
this.RequestId = m.Id; | ||
this._m = m; | ||
this.PosRefId = m.Data.pos_ref_id; | ||
this._receiptToSign = m.Data.merchant_receipt; | ||
} | ||
SignatureRequired(posRefId, requestId, receiptToSign) | ||
{ | ||
this.RequestId = requestId; | ||
this.PosRefId = posRefId; | ||
this._receiptToSign = receiptToSign; | ||
} | ||
GetMerchantReceipt() | ||
{ | ||
return this._m.Data.merchant_receipt; | ||
return this._receiptToSign; | ||
} | ||
@@ -249,5 +469,5 @@ } | ||
{ | ||
constructor(signatureRequiredRequestId) | ||
constructor(posRefId) | ||
{ | ||
this.SignatureRequiredRequestId = signatureRequiredRequestId; | ||
this.PosRefId = posRefId; | ||
} | ||
@@ -257,3 +477,6 @@ | ||
{ | ||
return new Message(this.SignatureRequiredRequestId, Events.SignatureDeclined, null, true); | ||
var data = { | ||
pos_ref_id: this.PosRefId | ||
}; | ||
return new Message(RequestIdHelper.Id("sigdec"), Events.SignatureDeclined, data, true); | ||
} | ||
@@ -264,5 +487,5 @@ } | ||
{ | ||
constructor(signatureRequiredRequestId) | ||
constructor(posRefId) | ||
{ | ||
this.SignatureRequiredRequestId = signatureRequiredRequestId; | ||
this.PosRefId = posRefId; | ||
} | ||
@@ -272,4 +495,84 @@ | ||
{ | ||
return new Message(this.SignatureRequiredRequestId, Events.SignatureAccepted, null, true); | ||
var data = { | ||
pos_ref_id: this.PosRefId | ||
}; | ||
return new Message(RequestIdHelper.Id("sigacc"), Events.SignatureAccepted, data, true); | ||
} | ||
} | ||
class MotoPurchaseRequest | ||
{ | ||
constructor(amountCents, posRefId) | ||
{ | ||
this.PosRefId = posRefId; | ||
this.PurchaseAmount = amountCents; | ||
this.Config = new SpiConfig(); | ||
} | ||
ToMessage() | ||
{ | ||
var data = { | ||
pos_ref_id: this.PosRefId, | ||
purchase_amount: this.PurchaseAmount | ||
}; | ||
this.Config.addReceiptConfig(data); | ||
return new Message(RequestIdHelper.Id("moto"), Events.MotoPurchaseRequest, data, true); | ||
} | ||
} | ||
class MotoPurchaseResponse | ||
{ | ||
constructor(m) | ||
{ | ||
this.PurchaseResponse = new PurchaseResponse(m); | ||
this.PosRefId = PurchaseResponse.PosRefId; | ||
} | ||
} | ||
class PhoneForAuthRequired | ||
{ | ||
constructor(...args) | ||
{ | ||
if(args.length === 4) { | ||
this.PosRefId = args[0]; | ||
this.RequestId = args[1]; | ||
this._phoneNumber = args[2]; | ||
this._merchantId = args[3]; | ||
} else if(args.length === 1) { | ||
this.RequestId = args[0].Id; | ||
this.PosRefId = args[0].Data.pos_ref_id; | ||
this._phoneNumber = args[0].Data.auth_centre_phone_number; | ||
this._merchantId = args[0].Data.merchant_id; | ||
} else { | ||
throw new Error('Invalid call sig for Phone auth required class'); | ||
} | ||
} | ||
GetPhoneNumber() | ||
{ | ||
return this._phoneNumber; | ||
} | ||
GetMerchantId() | ||
{ | ||
return this._merchantId; | ||
} | ||
} | ||
class AuthCodeAdvice | ||
{ | ||
constructor(posRefId, authCode) | ||
{ | ||
this.PosRefId = posRefId; | ||
this.AuthCode = authCode; | ||
} | ||
ToMessage() | ||
{ | ||
var data = { | ||
pos_ref_id: this.PosRefId, | ||
auth_code: this.AuthCode | ||
}; | ||
return new Message(RequestIdHelper.Id("authad"), Events.AuthCodeAdvice, data, true); | ||
} | ||
} |
@@ -8,2 +8,14 @@ class PurchaseHelper | ||
static CreatePurchaseRequestV2(posRefId, purchaseAmount, tipAmount, cashoutAmount, promptForCashout) | ||
{ | ||
var pr = Object.assign(new PurchaseRequest(purchaseAmount, posRefId), | ||
{ | ||
CashoutAmount: cashoutAmount, | ||
TipAmount: tipAmount, | ||
PromptForCashout: promptForCashout | ||
}); | ||
return pr; | ||
} | ||
static CreateRefundRequest(amountCents, purchaseId) | ||
@@ -10,0 +22,0 @@ { |
@@ -7,3 +7,3 @@ class SettleRequest { | ||
ToMessage() { | ||
return new Message(RequestIdHelper.Id("stl"), Events.SettleRequest, null, true); | ||
return new Message(this.Id, Events.SettleRequest, null, true); | ||
} | ||
@@ -19,2 +19,43 @@ } | ||
GetSettleByAcquirerCount() | ||
{ | ||
return this._m.Data.accumulated_settle_by_acquirer_count; | ||
} | ||
GetSettleByAcquirerValue() | ||
{ | ||
return this._m.Data.accumulated_settle_by_acquirer_value; | ||
} | ||
GetTotalCount() | ||
{ | ||
return this._m.Data.accumulated_total_count; | ||
} | ||
GetTotalValue() | ||
{ | ||
return this._m.Data.accumulated_total_value; | ||
} | ||
GetPeriodStartTime() | ||
{ | ||
var timeStr = this._m.Data.settlement_period_start_time; // "05:00" | ||
var dateStr = this._m.Data.settlement_period_start_date; // "05Oct17" | ||
return Message.ParseBankDateTimeStr(dateStr, timeStr); | ||
} | ||
GetPeriodEndTime() | ||
{ | ||
var timeStr = this._m.Data.settlement_period_end_time; // "05:00" | ||
var dateStr = this._m.Data.settlement_period_end_date; // "05Oct17" | ||
return Message.ParseBankDateTimeStr(dateStr, timeStr); | ||
} | ||
GetTriggeredTime() | ||
{ | ||
var timeStr = this._m.Data.settlement_triggered_time; // "05:00:45" | ||
var dateStr = this._m.Data.settlement_triggered_date; // "05Oct17" | ||
return Message.ParseBankDateTimeStr(dateStr, timeStr); | ||
} | ||
GetResponseText() | ||
@@ -29,2 +70,60 @@ { | ||
} | ||
GetTransactionRange() | ||
{ | ||
return this._m.Data.transaction_range; | ||
} | ||
GetTerminalId() | ||
{ | ||
return this._m.Data.terminal_id; | ||
} | ||
GetSchemeSettlementEntries() | ||
{ | ||
var schemes = this._m.Data.schemes; | ||
if (!schemes) return []; | ||
return schemes.map((scheme) => { | ||
return new SchemeSettlementEntry(scheme); | ||
}); | ||
} | ||
} | ||
class SchemeSettlementEntry | ||
{ | ||
// SchemeSettlementEntry(string schemeName, bool settleByAcquirer, int totalCount, int totalValue) | ||
// SchemeSettlementEntry(Object schemeObj) | ||
constructor(...args) | ||
{ | ||
if(args.length === 1) { | ||
this.SchemeName = args[0].scheme_name; | ||
this.SettleByAcquirer = args[0].settle_by_acquirer.toLowerCase() == "yes"; | ||
this.TotalValue = parseInt(args[0].total_value,10); | ||
this.TotalCount = parseInt(args[0].total_count,10); | ||
} else if(args.length === 4) { | ||
this.SchemeName = args[0]; | ||
this.SettleByAcquirer = args[1]; | ||
this.TotalCount = args[2]; | ||
this.TotalValue = args[3]; | ||
} | ||
} | ||
ToString() | ||
{ | ||
return `SchemeName: ${this.SchemeName}, SettleByAcquirer: ${this.SettleByAcquirer}, TotalCount: ${this.TotalCount}, TotalValue: ${this.TotalValue}`; | ||
} | ||
} | ||
class SettlementEnquiryRequest | ||
{ | ||
constructor(id) | ||
{ | ||
this.Id = id; | ||
} | ||
ToMessage() | ||
{ | ||
return new Message(this.Id, Events.SettlementEnquiryRequest, null, true); | ||
} | ||
} |
932
src/Spi.js
@@ -16,3 +16,3 @@ class Spi { | ||
constructor(posId, eftposAddress, secrets, log) | ||
constructor(posId, eftposAddress, secrets) | ||
{ | ||
@@ -23,2 +23,3 @@ this._posId = posId; | ||
this._log = console; | ||
this.Config = new SpiConfig(); | ||
@@ -48,9 +49,18 @@ // Our stamp for signing outgoing messages | ||
this.CurrentTxFlowState = null; | ||
} | ||
this._resetConn(); | ||
EnablePayAtTable() | ||
{ | ||
this._spiPat = new SpiPayAtTable(this); | ||
return this._spiPat; | ||
} | ||
EnablePreauth() | ||
{ | ||
this._spiPreauth = new SpiPreauth(this); | ||
return this._spiPreauth; | ||
} | ||
Start() { | ||
this._resetConn(); | ||
this._startTransactionMonitoringThread(); | ||
@@ -61,2 +71,3 @@ | ||
{ | ||
this._log.info("Starting in Paired State"); | ||
this._currentStatus = SpiStatus.PairedConnecting; | ||
@@ -67,2 +78,3 @@ this._conn.Connect(); // This is non-blocking | ||
{ | ||
this._log.info("Starting in Unpaired State"); | ||
this._currentStatus = SpiStatus.Unpaired; | ||
@@ -72,6 +84,6 @@ } | ||
/// <summary> | ||
/// Allows you to set the PosId which identifies this instance of your POS. | ||
/// Can only be called in thge Unpaired state. | ||
/// </summary> | ||
// <summary> | ||
// Allows you to set the PosId which identifies this instance of your POS. | ||
// Can only be called in thge Unpaired state. | ||
// </summary> | ||
SetPosId(posId) | ||
@@ -87,7 +99,7 @@ { | ||
/// <summary> | ||
/// Allows you to set the PinPad address. Sometimes the PinPad might change IP address | ||
/// (we recommend reserving static IPs if possible). | ||
/// Either way you need to allow your User to enter the IP address of the PinPad. | ||
/// </summary> | ||
// <summary> | ||
// Allows you to set the PinPad address. Sometimes the PinPad might change IP address | ||
// (we recommend reserving static IPs if possible). | ||
// Either way you need to allow your User to enter the IP address of the PinPad. | ||
// </summary> | ||
SetEftposAddress(address) | ||
@@ -104,9 +116,9 @@ { | ||
/// <summary> | ||
/// Call this one when a flow is finished and you want to go back to idle state. | ||
/// Typically when your user clicks the "OK" bubtton to acknowldge that pairing is | ||
/// finished, or that transaction is finished. | ||
/// When true, you can dismiss the flow screen and show back the idle screen. | ||
/// </summary> | ||
/// <returns>true means we have moved back to the Idle state. false means current flow was not finished yet.</returns> | ||
// <summary> | ||
// Call this one when a flow is finished and you want to go back to idle state. | ||
// Typically when your user clicks the "OK" bubtton to acknowldge that pairing is | ||
// finished, or that transaction is finished. | ||
// When true, you can dismiss the flow screen and show back the idle screen. | ||
// </summary> | ||
// <returns>true means we have moved back to the Idle state. false means current flow was not finished yet.</returns> | ||
AckFlowEndedAndBackToIdle() | ||
@@ -132,13 +144,27 @@ { | ||
/// <summary> | ||
/// This will connect to the Eftpos and start the pairing process. | ||
/// Only call this if you are in the Unpaired state. | ||
/// Subscribe to the PairingFlowStateChanged event to get updates on the pairing process. | ||
/// </summary> | ||
static GetVersion() | ||
{ | ||
return '2.1.0'; | ||
} | ||
// endregion | ||
// <summary> | ||
// This will connect to the Eftpos and start the pairing process. | ||
// Only call this if you are in the Unpaired state. | ||
// Subscribe to the PairingFlowStateChanged event to get updates on the pairing process. | ||
// </summary> | ||
// <returns>Whether pairing has initiated or not</returns> | ||
Pair() | ||
{ | ||
if (this.CurrentStatus != SpiStatus.Unpaired) { | ||
return; | ||
this._log.warn("Tried to Pair but we're already so."); | ||
return false; | ||
} | ||
if (!this._posId || !this._eftposAddress) | ||
{ | ||
this._log.warn("Tried to Pair but missing posId or eftposAddress"); | ||
return false; | ||
} | ||
this.CurrentFlow = SpiFlow.Pairing; | ||
@@ -157,8 +183,9 @@ this.CurrentPairingFlowState = new PairingFlowState | ||
this._conn.Connect(); // Non-Blocking | ||
return true; | ||
} | ||
/// <summary> | ||
/// Call this when your user clicks yes to confirm the pairing code on your | ||
/// screen matches the one on the Eftpos. | ||
/// </summary> | ||
// <summary> | ||
// Call this when your user clicks yes to confirm the pairing code on your | ||
// screen matches the one on the Eftpos. | ||
// </summary> | ||
PairingConfirmCode() | ||
@@ -176,4 +203,6 @@ { | ||
// But we are still waiting for confirmation from Eftpos side. | ||
this._log.info("Pair Code Confirmed from POS side, but am still waiting for confirmation from Eftpos."); | ||
this.CurrentPairingFlowState.Message = | ||
"Click YES on EFTPOS if code is: " + this.CurrentPairingFlowState.ConfirmationCode; | ||
document.dispatchEvent(new CustomEvent('PairingFlowStateChanged', {detail: this.CurrentPairingFlowState})); | ||
} | ||
@@ -183,12 +212,11 @@ else | ||
// Already confirmed from Eftpos - So all good now. We're Paired also from the POS perspective. | ||
this.CurrentPairingFlowState.Message = "Pairing Successful"; | ||
this._log.info("Pair Code Confirmed from POS side, and was already confirmed from Eftpos side. Pairing finalised."); | ||
this._onPairingSuccess(); | ||
this._onReadyToTransact(); | ||
} | ||
document.dispatchEvent(new CustomEvent('PairingFlowStateChanged', {detail: this.CurrentPairingFlowState})); | ||
} | ||
/// <summary> | ||
/// Call this if your user clicks CANCEL or NO during the pairing process. | ||
/// </summary> | ||
// <summary> | ||
// Call this if your user clicks CANCEL or NO during the pairing process. | ||
// </summary> | ||
PairingCancel() | ||
@@ -200,14 +228,17 @@ { | ||
this.CurrentPairingFlowState.Message = "Pairing Canelled"; | ||
if (this.CurrentPairingFlowState.AwaitingCheckFromPos && !this.CurrentPairingFlowState.AwaitingCheckFromEftpos) | ||
{ | ||
// This means that the Eftpos already thinks it's paired. | ||
// Let's tell it to drop keys | ||
this._send(new DropKeysRequest().ToMessage()); | ||
} | ||
this._onPairingFailed(); | ||
document.dispatchEvent(new CustomEvent('PairingFlowStateChanged', {detail: this.CurrentPairingFlowState})); | ||
} | ||
/// <summary> | ||
/// Call this when your uses clicks the Unpair button. | ||
/// This will disconnect from the Eftpos and forget the secrets. | ||
/// The CurrentState is then changed to Unpaired. | ||
/// Call this only if you are not yet in the Unpaired state. | ||
/// </summary> | ||
// <summary> | ||
// Call this when your uses clicks the Unpair button. | ||
// This will disconnect from the Eftpos and forget the secrets. | ||
// The CurrentState is then changed to Unpaired. | ||
// Call this only if you are not yet in the Unpaired state. | ||
// </summary> | ||
Unpair() | ||
@@ -223,18 +254,19 @@ { | ||
this.CurrentStatus = SpiStatus.Unpaired; | ||
this._conn.Disconnect(); | ||
this._secrets = null; | ||
this._spiMessageStamp.Secrets = null; | ||
document.dispatchEvent(new CustomEvent('SecretsChanged', {detail: this._secrets})); | ||
// Best effort letting the eftpos know that we're dropping the keys, so it can drop them as well. | ||
this._send(new DropKeysRequest().ToMessage()); | ||
this._doUnpair(); | ||
return true; | ||
} | ||
/// <summary> | ||
/// Initiates a purchase transaction. Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
/// </summary> | ||
/// <param name="id">Alphanumeric Identifier for your purchase.</param> | ||
/// <param name="amountCents">Amount in Cents to charge</param> | ||
/// <returns>InitiateTxResult</returns> | ||
InitiatePurchaseTx(id, amountCents) | ||
// endregion | ||
// region Transaction Methods | ||
// <summary> | ||
// Initiates a purchase transaction. Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
// </summary> | ||
// <param name="posRefId">Alphanumeric Identifier for your purchase.</param> | ||
// <param name="amountCents">Amount in Cents to charge</param> | ||
// <returns>InitiateTxResult</returns> | ||
InitiatePurchaseTx(posRefId, amountCents) | ||
{ | ||
@@ -249,8 +281,10 @@ if (this.CurrentStatus == SpiStatus.Unpaired) { | ||
var purchase = PurchaseHelper.CreatePurchaseRequest(amountCents, id); | ||
var purchaseRequest = PurchaseHelper.CreatePurchaseRequest(amountCents, posRefId); | ||
purchaseRequest.Config = this.Config; | ||
var purchaseMsg = purchaseRequest.ToMessage(); | ||
this.CurrentFlow = SpiFlow.Transaction; | ||
this.CurrentTxFlowState = new TransactionFlowState( | ||
id, TransactionType.Purchase, amountCents, purchase.ToMessage(), | ||
posRefId, TransactionType.Purchase, amountCents, purchaseMsg, | ||
`Waiting for EFTPOS connection to make payment request for ${amountCents / 100.0}`); | ||
if (this._send(purchase.ToMessage())) | ||
if (this._send(purchaseMsg)) | ||
{ | ||
@@ -264,10 +298,44 @@ this.CurrentTxFlowState.Sent(`Asked EFTPOS to accept payment for ${amountCents / 100.0}`); | ||
/// <summary> | ||
/// Initiates a refund transaction. Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
/// </summary> | ||
/// <param name="id">Alphanumeric Identifier for your refund.</param> | ||
/// <param name="amountCents">Amount in Cents to charge</param> | ||
/// <returns>InitiateTxResult</returns> | ||
InitiateRefundTx(id, amountCents) | ||
// <summary> | ||
// Initiates a purchase transaction. Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
// <para>Tip and cashout are not allowed simultaneously.</para> | ||
// </summary> | ||
// <param name="posRefId">An Unique Identifier for your Order/Purchase</param> | ||
// <param name="purchaseAmount">The Purchase Amount in Cents.</param> | ||
// <param name="tipAmount">The Tip Amount in Cents</param> | ||
// <param name="cashoutAmount">The Cashout Amount in Cents</param> | ||
// <param name="promptForCashout">Whether to prompt your customer for cashout on the Eftpos</param> | ||
// <returns>InitiateTxResult</returns> | ||
InitiatePurchaseTxV2(posRefId, purchaseAmount, tipAmount, cashoutAmount, promptForCashout) | ||
{ | ||
if (this.CurrentStatus == SpiStatus.Unpaired) return new InitiateTxResult(false, "Not Paired"); | ||
if (tipAmount > 0 && (cashoutAmount > 0 || promptForCashout)) return new InitiateTxResult(false, "Cannot Accept Tips and Cashout at the same time."); | ||
if (this.CurrentFlow != SpiFlow.Idle) return new InitiateTxResult(false, "Not Idle"); | ||
this.CurrentFlow = SpiFlow.Transaction; | ||
var purchase = PurchaseHelper.CreatePurchaseRequestV2(posRefId, purchaseAmount, tipAmount, cashoutAmount, promptForCashout); | ||
purchase.Config = this.Config; | ||
var purchaseMsg = purchase.ToMessage(); | ||
this.CurrentTxFlowState = new TransactionFlowState( | ||
posRefId, TransactionType.Purchase, purchaseAmount, purchaseMsg, | ||
`Waiting for EFTPOS connection to make payment request. ${purchase.AmountSummary()}`); | ||
if (this._send(purchaseMsg)) | ||
{ | ||
this.CurrentTxFlowState.Sent(`Asked EFTPOS to accept payment for ${purchase.AmountSummary()}`); | ||
} | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
return new InitiateTxResult(true, "Purchase Initiated"); | ||
} | ||
// <summary> | ||
// Initiates a refund transaction. Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
// </summary> | ||
// <param name="posRefId">Alphanumeric Identifier for your refund.</param> | ||
// <param name="amountCents">Amount in Cents to charge</param> | ||
// <returns>InitiateTxResult</returns> | ||
InitiateRefundTx(posRefId, amountCents) | ||
{ | ||
if (this.CurrentStatus == SpiStatus.Unpaired) { | ||
@@ -281,10 +349,12 @@ return new InitiateTxResult(false, "Not Paired"); | ||
var purchase = PurchaseHelper.CreateRefundRequest(amountCents, id); | ||
var refundRequest = PurchaseHelper.CreateRefundRequest(amountCents, posRefId); | ||
refundRequest.Config = this.Config; | ||
var refundMsg = refundRequest.ToMessage(); | ||
this.CurrentFlow = SpiFlow.Transaction; | ||
this.CurrentTxFlowState = new TransactionFlowState( | ||
id, TransactionType.Refund, amountCents, purchase.ToMessage(), | ||
`Waiting for EFTPOS connection to make refund request for ${amountCents / 100.0}`); | ||
if (this._send(purchase.ToMessage())) | ||
posRefId, TransactionType.Refund, amountCents, refundMsg, | ||
`Waiting for EFTPOS connection to make refund request for ${(amountCents / 100.0).toFixed(2)}`); | ||
if (this._send(refundMsg)) | ||
{ | ||
this.CurrentTxFlowState.Sent(`Asked EFTPOS to refund ${amountCents / 100.0}`); | ||
this.CurrentTxFlowState.Sent(`Asked EFTPOS to refund ${(amountCents / 100.0).toFixed(2)}`); | ||
} | ||
@@ -296,13 +366,12 @@ | ||
/// <summary> | ||
/// Let the EFTPOS know whether merchant accepted or declined the signature | ||
/// </summary> | ||
/// <param name="accepted">whether merchant accepted the signature from customer or not</param> | ||
// <summary> | ||
// Let the EFTPOS know whether merchant accepted or declined the signature | ||
// </summary> | ||
// <param name="accepted">whether merchant accepted the signature from customer or not</param> | ||
AcceptSignature(accepted) | ||
{ | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished || !this.CurrentTxFlowState.AwaitingSignatureCheck) | ||
{ | ||
this._log.info("Asked to accept signature but I was not waiting for one."); | ||
return; | ||
return new MidTxResult(false, "Asked to accept signature but I was not waiting for one."); | ||
} | ||
@@ -313,13 +382,43 @@ | ||
this._send(accepted | ||
? new SignatureAccept(sigReqMsg.RequestId).ToMessage() | ||
: new SignatureDecline(sigReqMsg.RequestId).ToMessage()); | ||
? new SignatureAccept(this.CurrentTxFlowState.PosRefId).ToMessage() | ||
: new SignatureDecline(this.CurrentTxFlowState.PosRefId).ToMessage()); | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
return new MidTxResult(true, ""); | ||
} | ||
/// <summary> | ||
/// Attempts to cancel a Transaction. | ||
/// Be subscribed to TxFlowStateChanged event to see how it goes. | ||
/// Wait for the transaction to be finished and then see whether cancellation was successful or not. | ||
/// </summary> | ||
// <summary> | ||
// Submit the Code obtained by your user when phoning for auth. | ||
// It will return immediately to tell you whether the code has a valid format or not. | ||
// If valid==true is returned, no need to do anything else. Expect updates via standard callback. | ||
// If valid==false is returned, you can show your user the accompanying message, and invite them to enter another code. | ||
// </summary> | ||
// <param name="authCode">The code obtained by your user from the merchant call centre. It should be a 6-character alpha-numeric value.</param> | ||
// <returns>Whether code has a valid format or not.</returns> | ||
SubmitAuthCode(authCode) | ||
{ | ||
if (authCode.length != 6) | ||
{ | ||
return new SubmitAuthCodeResult(false, "Not a 6-digit code."); | ||
} | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished || !this.CurrentTxFlowState.AwaitingPhoneForAuth) | ||
{ | ||
this._log.info("Asked to send auth code but I was not waiting for one."); | ||
return new SubmitAuthCodeResult(false, "Was not waiting for one."); | ||
} | ||
this.CurrentTxFlowState.AuthCodeSent(`Submitting Auth Code ${authCode}`); | ||
this._send(new AuthCodeAdvice(this.CurrentTxFlowState.PosRefId, authCode).ToMessage()); | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
return new SubmitAuthCodeResult(true, "Valid Code."); | ||
} | ||
// <summary> | ||
// Attempts to cancel a Transaction. | ||
// Be subscribed to TxFlowStateChanged event to see how it goes. | ||
// Wait for the transaction to be finished and then see whether cancellation was successful or not. | ||
// </summary> | ||
// <returns>MidTxResult - false only if you called it in the wrong state</returns> | ||
CancelTransaction() | ||
@@ -330,3 +429,3 @@ { | ||
this._log.info("Asked to cancel transaction but I was not in the middle of one."); | ||
return; | ||
return new MidTxResult(false, "Asked to cancel transaction but I was not in the middle of one."); | ||
} | ||
@@ -348,10 +447,65 @@ | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
return new MidTxResult(true, ""); | ||
} | ||
/// <summary> | ||
/// Initiates a settlement transaction. | ||
/// Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
/// </summary> | ||
InitiateSettleTx(id) | ||
// <summary> | ||
// Initiates a cashout only transaction. Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
// </summary> | ||
// <param name="posRefId">Alphanumeric Identifier for your transaction.</param> | ||
// <param name="amountCents">Amount in Cents to cash out</param> | ||
// <returns>InitiateTxResult</returns> | ||
InitiateCashoutOnlyTx(posRefId, amountCents) | ||
{ | ||
if (this.CurrentStatus == SpiStatus.Unpaired) return new InitiateTxResult(false, "Not Paired"); | ||
if (this.CurrentFlow != SpiFlow.Idle) return new InitiateTxResult(false, "Not Idle"); | ||
var cashoutOnlyRequest = new CashoutOnlyRequest(amountCents, posRefId); | ||
cashoutOnlyRequest.Config = this.Config; | ||
var cashoutMsg = cashoutOnlyRequest.ToMessage(); | ||
this.CurrentFlow = SpiFlow.Transaction; | ||
this.CurrentTxFlowState = new TransactionFlowState( | ||
posRefId, TransactionType.CashoutOnly, amountCents, cashoutMsg, | ||
`Waiting for EFTPOS connection to send cashout request for ${(amountCents / 100).toFixed(2)}`); | ||
if (this._send(cashoutMsg)) | ||
{ | ||
this.CurrentTxFlowState.Sent(`Asked EFTPOS to do cashout for ${(amountCents / 100).toFixed(2)}`); | ||
} | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
return new InitiateTxResult(true, "Cashout Initiated"); | ||
} | ||
// <summary> | ||
// Initiates a Mail Order / Telephone Order Purchase Transaction | ||
// </summary> | ||
// <param name="posRefId">Alphanumeric Identifier for your transaction.</param> | ||
// <param name="amountCents">Amount in Cents</param> | ||
// <returns>InitiateTxResult</returns> | ||
InitiateMotoPurchaseTx(posRefId, amountCents) | ||
{ | ||
if (this.CurrentStatus == SpiStatus.Unpaired) return new InitiateTxResult(false, "Not Paired"); | ||
if (this.CurrentFlow != SpiFlow.Idle) return new InitiateTxResult(false, "Not Idle"); | ||
var motoPurchaseRequest = new MotoPurchaseRequest(amountCents, posRefId); | ||
motoPurchaseRequest.Config = this.Config; | ||
var cashoutMsg = motoPurchaseRequest.ToMessage(); | ||
this.CurrentFlow = SpiFlow.Transaction; | ||
this.CurrentTxFlowState = new TransactionFlowState( | ||
posRefId, TransactionType.MOTO, amountCents, cashoutMsg, | ||
`Waiting for EFTPOS connection to send MOTO request for ${(amountCents / 100).toFixed(2)}`); | ||
if (this._send(cashoutMsg)) | ||
{ | ||
this.CurrentTxFlowState.Sent(`Asked EFTPOS do MOTO for ${(amountCents / 100).toFixed(2)}`); | ||
} | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
return new InitiateTxResult(true, "MOTO Initiated"); | ||
} | ||
// <summary> | ||
// Initiates a settlement transaction. | ||
// Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
// </summary> | ||
InitiateSettleTx(posRefId) | ||
{ | ||
if (this.CurrentStatus == SpiStatus.Unpaired) { | ||
@@ -365,9 +519,9 @@ return new InitiateTxResult(false, "Not Paired"); | ||
var settleRequest = new SettleRequest(RequestIdHelper.Id("settle")); | ||
var settleRequestMsg = new SettleRequest(RequestIdHelper.Id("settle")).ToMessage(); | ||
this.CurrentFlow = SpiFlow.Transaction; | ||
this.CurrentTxFlowState = new TransactionFlowState( | ||
id, TransactionType.Settle, 0, settleRequest.ToMessage(), | ||
posRefId, TransactionType.Settle, 0, settleRequestMsg, | ||
`Waiting for EFTPOS connection to make a settle request`); | ||
if (this._send(settleRequest.ToMessage())) | ||
if (this._send(settleRequestMsg)) | ||
{ | ||
@@ -381,9 +535,30 @@ this.CurrentTxFlowState.Sent(`Asked EFTPOS to settle.`); | ||
/// <summary> | ||
/// Initiates a Get Last Transaction. Use this when you want to retrieve the most recent transaction | ||
/// that was processed by the Eftpos. | ||
/// Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
/// </summary> | ||
InitiateGetLastTx(id) | ||
// <summary> | ||
// </summary> | ||
InitiateSettlementEnquiry(posRefId) | ||
{ | ||
if (this.CurrentStatus == SpiStatus.Unpaired) return new InitiateTxResult(false, "Not Paired"); | ||
if (this.CurrentFlow != SpiFlow.Idle) return new InitiateTxResult(false, "Not Idle"); | ||
var stlEnqMsg = new SettlementEnquiryRequest(RequestIdHelper.Id("stlenq")).ToMessage(); | ||
this.CurrentFlow = SpiFlow.Transaction; | ||
this.CurrentTxFlowState = new TransactionFlowState( | ||
posRefId, TransactionType.SettlementEnquiry, 0, stlEnqMsg, | ||
"Waiting for EFTPOS connection to make a settlement enquiry"); | ||
if (this._send(stlEnqMsg)) | ||
{ | ||
this.CurrentTxFlowState.Sent("Asked EFTPOS to make a settlement enquiry."); | ||
} | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
return new InitiateTxResult(true,"Settle Initiated"); | ||
} | ||
// <summary> | ||
// Initiates a Get Last Transaction. Use this when you want to retrieve the most recent transaction | ||
// that was processed by the Eftpos. | ||
// Be subscribed to TxFlowStateChanged event to get updates on the process. | ||
// </summary> | ||
InitiateGetLastTx() | ||
{ | ||
if (this.CurrentStatus == SpiStatus.Unpaired) { | ||
@@ -397,10 +572,10 @@ return new InitiateTxResult(false, "Not Paired"); | ||
var gltRequestMsg = new GetLastTransactionRequest(id); | ||
var gltRequestMsg = new GetLastTransactionRequest().ToMessage(); | ||
this.CurrentFlow = SpiFlow.Transaction; | ||
var posRefId = gltRequestMsg.Id; // GetLastTx is not trying to get anything specific back. So we just use the message id. | ||
this.CurrentTxFlowState = new TransactionFlowState( | ||
gltRequestMsg.Id, TransactionType.GetLastTx, 0, gltRequestMsg, | ||
posRefId, TransactionType.GetLastTransaction, 0, gltRequestMsg, | ||
"Waiting for EFTPOS connection to make a Get-Last-Transaction request."); | ||
if (this._send(gltRequestMsg.ToMessage())) | ||
if (this._send(gltRequestMsg)) | ||
{ | ||
@@ -414,7 +589,74 @@ this.CurrentTxFlowState.Sent(`Asked EFTPOS for last transaction.`); | ||
// <summary> | ||
// This is useful to recover from your POS crashing in the middle of a transaction. | ||
// When you restart your POS, if you had saved enough state, you can call this method to recover the client library state. | ||
// You need to have the posRefId that you passed in with the original transaction, and the transaction type. | ||
// This method will return immediately whether recovery has started or not. | ||
// If recovery has started, you need to bring up the transaction modal to your user a be listening to TxFlowStateChanged. | ||
// </summary> | ||
// <param name="posRefId">The is that you had assigned to the transaction that you are trying to recover.</param> | ||
// <param name="txType">The transaction type.</param> | ||
// <returns></returns> | ||
InitiateRecovery(posRefId, txType) | ||
{ | ||
if (this.CurrentStatus == SpiStatus.Unpaired) return new InitiateTxResult(false, "Not Paired"); | ||
if (this.CurrentFlow != SpiFlow.Idle) return new InitiateTxResult(false, "Not Idle"); | ||
this.CurrentFlow = SpiFlow.Transaction; | ||
var gltRequestMsg = new GetLastTransactionRequest().ToMessage(); | ||
this.CurrentTxFlowState = new TransactionFlowState( | ||
posRefId, txType, 0, gltRequestMsg, | ||
"Waiting for EFTPOS connection to attempt recovery."); | ||
if (this._send(gltRequestMsg)) | ||
{ | ||
this.CurrentTxFlowState.Sent(`Asked EFTPOS to recover state.`); | ||
} | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
return new InitiateTxResult(true, "Recovery Initiated"); | ||
} | ||
/// <summary> | ||
/// Handling the 2nd interaction of the pairing process, i.e. an incoming KeyRequest. | ||
/// </summary> | ||
/// <param name="m">incoming message</param> | ||
// <summary> | ||
// GltMatch attempts to conclude whether a gltResponse matches an expected transaction and returns | ||
// the outcome. | ||
// If Success/Failed is returned, it means that the gtlResponse did match, and that transaction was succesful/failed. | ||
// If Unknown is returned, it means that the gltResponse does not match the expected transaction. | ||
// </summary> | ||
// <param name="gltResponse">The GetLastTransactionResponse message to check</param> | ||
// <param name="posRefId">The Reference Id that you passed in with the original request.</param> | ||
// <returns></returns> | ||
GltMatch(gltResponse, posRefId, ...deprecatedArgs) | ||
{ | ||
// Obsolete method call check | ||
// Old interface: GltMatch(GetLastTransactionResponse gltResponse, TransactionType expectedType, int expectedAmount, DateTime requestTime, string posRefId) | ||
if(deprecatedArgs.length) { | ||
if(deprecatedArgs.length == 2) { | ||
this._log.info("Obsolete method call detected: Use GltMatch(gltResponse, posRefId)"); | ||
return this.GltMatch(gltResponse, deprecatedArgs[2]); | ||
} else { | ||
throw new Error("Obsolete method call with unknown args: Use GltMatch(GetLastTransactionResponse gltResponse, string posRefId)"); | ||
} | ||
} | ||
this._log.info(`GLT CHECK: PosRefId: ${posRefId}->${gltResponse.GetPosRefId()}`); | ||
if (!posRefId == gltResponse.GetPosRefId()) | ||
{ | ||
return SuccessState.Unknown; | ||
} | ||
return gltResponse.GetSuccessState(); | ||
} | ||
// endregion | ||
// region Internals for Pairing Flow | ||
// <summary> | ||
// Handling the 2nd interaction of the pairing process, i.e. an incoming KeyRequest. | ||
// </summary> | ||
// <param name="m">incoming message</param> | ||
_handleKeyRequest(m) | ||
@@ -433,6 +675,6 @@ { | ||
/// <summary> | ||
/// Handling the 4th interaction of the pairing process i.e. an incoming KeyCheck. | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// Handling the 4th interaction of the pairing process i.e. an incoming KeyCheck. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleKeyCheck(m) | ||
@@ -448,7 +690,6 @@ { | ||
/// <summary> | ||
/// Handling the 5th and final interaction of the pairing process, i.e. an incoming PairResponse | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// Handling the 5th and final interaction of the pairing process, i.e. an incoming PairResponse | ||
// </summary> | ||
// <param name="m"></param> | ||
_handlePairResponse(m) | ||
@@ -464,7 +705,9 @@ { | ||
// Still Waiting for User to say yes on POS | ||
this._log.info("Got Pair Confirm from Eftpos, but still waiting for use to confirm from POS."); | ||
this.CurrentPairingFlowState.Message = "Confirm that the following Code is what the EFTPOS showed"; | ||
document.dispatchEvent(new CustomEvent('PairingFlowStateChanged', {detail: this.CurrentPairingFlowState})); | ||
} | ||
else | ||
{ | ||
this.CurrentPairingFlowState.Message = "Pairing Successful"; | ||
this._log.info("Got Pair Confirm from Eftpos, and already had confirm from POS. Now just waiting for first pong."); | ||
this._onPairingSuccess(); | ||
@@ -478,7 +721,10 @@ } | ||
{ | ||
this.CurrentPairingFlowState.Message = "Pairing Failed"; | ||
this._onPairingFailed(); | ||
} | ||
} | ||
document.dispatchEvent(new CustomEvent('PairingFlowStateChanged', {detail: this.CurrentPairingFlowState})); | ||
_handleDropKeysAdvice(m) | ||
{ | ||
this._log.Info("Eftpos was Unpaired. I shall unpair from my end as well."); | ||
this._doUnpair(); | ||
} | ||
@@ -510,6 +756,15 @@ | ||
/// <summary> | ||
/// Sometimes the server asks us to roll our secrets. | ||
/// </summary> | ||
/// <param name="m"></param> | ||
_doUnpair() | ||
{ | ||
this.CurrentStatus = SpiStatus.Unpaired; | ||
this._conn.Disconnect(); | ||
this._secrets = null; | ||
this._spiMessageStamp.Secrets = null; | ||
document.dispatchEvent(new CustomEvent('SecretsChanged', {detail: this._secrets})); | ||
} | ||
// <summary> | ||
// Sometimes the server asks us to roll our secrets. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleKeyRollingRequest(m) | ||
@@ -525,14 +780,14 @@ { | ||
/// <summary> | ||
/// The PinPad server will send us this message when a customer signature is reqired. | ||
/// We need to ask the customer to sign the incoming receipt. | ||
/// And then tell the pinpad whether the signature is ok or not. | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// The PinPad server will send us this message when a customer signature is reqired. | ||
// We need to ask the customer to sign the incoming receipt. | ||
// And then tell the pinpad whether the signature is ok or not. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleSignatureRequired(m) | ||
{ | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished) | ||
var incomingPosRefId = m.Data.pos_ref_id; | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished || !this.CurrentTxFlowState.PosRefId == incomingPosRefId) | ||
{ | ||
this._log.info(`Received Signature Required but I was not waiting for one. ${m.DecryptedJson}`); | ||
this._log.info(`Received Signature Required but I was not waiting for one. Incoming Pos Ref ID: ${incomingPosRefId}`); | ||
return; | ||
@@ -545,12 +800,31 @@ } | ||
/// <summary> | ||
/// The PinPad server will reply to our PurchaseRequest with a PurchaseResponse. | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// The PinPad server will send us this message when an auth code is required. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleAuthCodeRequired(m) | ||
{ | ||
var incomingPosRefId = m.Data.pos_ref_id; | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished || !this.CurrentTxFlowState.PosRefId == incomingPosRefId) | ||
{ | ||
_log.Info(`Received Auth Code Required but I was not waiting for one. Incoming Pos Ref ID: ${incomingPosRefId}`); | ||
return; | ||
} | ||
var phoneForAuthRequired = new PhoneForAuthRequired(m); | ||
var msg = `Auth Code Required. Call ${phoneForAuthRequired.GetPhoneNumber()} and quote merchant id ${phoneForAuthRequired.GetMerchantId()}`; | ||
this.CurrentTxFlowState.PhoneForAuthRequired(phoneForAuthRequired, msg); | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
} | ||
// <summary> | ||
// The PinPad server will reply to our PurchaseRequest with a PurchaseResponse. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handlePurchaseResponse(m) | ||
{ | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished) | ||
var incomingPosRefId = m.Data.pos_ref_id; | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished || !this.CurrentTxFlowState.PosRefId == incomingPosRefId) | ||
{ | ||
this._log.info(`Received Purchase response but I was not waiting for one. ${m.DecryptedJson}`); | ||
this._log.info(`Received Purchase response but I was not waiting for one. Incoming Pos Ref ID: ${incomingPosRefId}"`); | ||
return; | ||
@@ -566,11 +840,52 @@ } | ||
/// <summary> | ||
/// The PinPad server will reply to our RefundRequest with a RefundResponse. | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// The PinPad server will reply to our CashoutOnlyRequest with a CashoutOnlyResponse. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleCashoutOnlyResponse(m) | ||
{ | ||
var incomingPosRefId = m.Data.pos_ref_id; | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished || !this.CurrentTxFlowState.PosRefId == incomingPosRefId) | ||
{ | ||
this._log.info(`Received Cashout Response but I was not waiting for one. Incoming Pos Ref ID: ${incomingPosRefId}`); | ||
return; | ||
} | ||
// TH-1A, TH-2A | ||
this.CurrentTxFlowState.Completed(m.GetSuccessState(), m, "Cashout Transaction Ended."); | ||
// TH-6A, TH-6E | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
} | ||
// <summary> | ||
// The PinPad server will reply to our MotoPurchaseRequest with a MotoPurchaseResponse. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleMotoPurchaseResponse(m) | ||
{ | ||
var incomingPosRefId = m.Data.pos_ref_id; | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished || !this.CurrentTxFlowState.PosRefId == incomingPosRefId) | ||
{ | ||
this._log.info(`Received Moto Response but I was not waiting for one. Incoming Pos Ref ID: ${incomingPosRefId}`); | ||
return; | ||
} | ||
// TH-1A, TH-2A | ||
this.CurrentTxFlowState.Completed(m.GetSuccessState(), m, "Moto Transaction Ended."); | ||
// TH-6A, TH-6E | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
} | ||
// <summary> | ||
// The PinPad server will reply to our RefundRequest with a RefundResponse. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleRefundResponse(m) | ||
{ | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished) | ||
var incomingPosRefId = m.Data.pos_ref_id; | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished | !this.CurrentTxFlowState.PosRefId == incomingPosRefId) | ||
{ | ||
this._log.info(`Received Refund response but I was not waiting for one. ${m.DecryptedJson}`); | ||
this._log.info(`Received Refund response but I was not waiting for this one. Incoming Pos Ref ID: ${incomingPosRefId}`); | ||
return; | ||
@@ -586,6 +901,6 @@ } | ||
/// <summary> | ||
/// TODO: Handle the Settlement Response received from the PinPad | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// TODO: Handle the Settlement Response received from the PinPad | ||
// </summary> | ||
// <param name="m"></param> | ||
HandleSettleResponse(m) | ||
@@ -606,6 +921,25 @@ { | ||
/// <summary> | ||
/// Sometimes we receive event type "error" from the server, such as when calling cancel_transaction and there is no transaction in progress. | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// Handle the Settlement Enquiry Response received from the PinPad | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleSettlementEnquiryResponse(m) | ||
{ | ||
if (this.CurrentFlow != SpiFlow.Transaction || this.CurrentTxFlowState.Finished) | ||
{ | ||
this._log.info(`Received Settlement Enquiry response but I was not waiting for one. ${m.DecryptedJson}`); | ||
return; | ||
} | ||
// TH-1A, TH-2A | ||
this.CurrentTxFlowState.Completed(m.GetSuccessState(), m, "Settlement Enquiry Ended."); | ||
// TH-6A, TH-6E | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
} | ||
// <summary> | ||
// Sometimes we receive event type "error" from the server, such as when calling cancel_transaction and there is no transaction in progress. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleErrorEvent(m) | ||
@@ -627,65 +961,7 @@ { | ||
} | ||
/// <summary> | ||
/// GltMatch attempts to conclude whether a gltResponse matches an expected transaction and returns | ||
/// the outcome. | ||
/// If Success/Failed is returned, it means that the gtlResponse did match, and that transaction was succesful/failed. | ||
/// If Unknown is returned, it means that the gtlResponse does not match the expected transaction. | ||
/// </summary> | ||
/// <param name="gltResponse">The GetLastTransactionResponse message to check</param> | ||
/// <param name="expectedType">The expected Type, Purchase/Refund/...</param> | ||
/// <param name="expectedAmount">The expected Amount in cents</param> | ||
/// <param name="requestTime">The time you made your request.</param> | ||
/// <param name="posRefId">The Reference Id that you passed in with the original request. Currently not used. | ||
/// But will be used in the future for abetter conclusion.</param> | ||
/// <returns></returns> | ||
GltMatch(gltResponse, expectedType, expectedAmount, requestTime, posRefId) { | ||
// adjust request time for serverTime and also give 5 seconds slack. | ||
var reqServerTime = new Date(requestTime + this._spiMessageStamp.ServerTimeDelta - 5000); | ||
/* Transformat into ISO format */ | ||
var bankDateTime = gltResponse.GetBankDateTimeString(); | ||
var gtlBankTime = new Date(bankDateTime.substring(4, 8) + "-" | ||
+ bankDateTime.substring(2, 4) + "-" | ||
+ bankDateTime.substring(0, 2) + "T" | ||
+ bankDateTime.substring(8, 10)+":"+ bankDateTime.substring(10, 12) +":"+bankDateTime.substring(12, 14)); | ||
// For now we use amount and date to match as best we can. | ||
// In the future we will be able to pass our own pos_ref_id in the tx request that will be returned here. | ||
this._log.info(`Amount: ${expectedAmount}->${gltResponse.GetTransactionAmount()}, Date: ${reqServerTime}->${gtlBankTime}`); | ||
if (gltResponse.GetTransactionAmount() != expectedAmount) | ||
{ | ||
return SuccessState.Unknown; | ||
} | ||
switch (gltResponse.GetTxType()) | ||
{ | ||
case "PURCHASE": | ||
if (expectedType != TransactionType.Purchase) | ||
return SuccessState.Unknown; | ||
break; | ||
case "REFUND": | ||
if (expectedType != TransactionType.Refund) | ||
return SuccessState.Unknown; | ||
break; | ||
default: | ||
return SuccessState.Unknown; | ||
} | ||
if (reqServerTime > gtlBankTime) | ||
{ | ||
return SuccessState.Unknown; | ||
} | ||
// For now we use amount and date to match as best we can. | ||
// In the future we will be able to pass our own pos_ref_id in the tx request that will be returned here. | ||
return gltResponse.GetSuccessState(); | ||
} | ||
/// <summary> | ||
/// When the PinPad returns to us what the Last Transaction was. | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// When the PinPad returns to us what the Last Transaction was. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleGetLastTransactionResponse(m) | ||
@@ -702,18 +978,35 @@ { | ||
// Let's attempt recovery. This is step 4 of Transaction Processing Handling | ||
this._log.info(`Got Last Transaction.. Attempting Recovery.`); | ||
this._log.info(`Got Last Transaction..`); | ||
txState.GotGltResponse(); | ||
var gtlResponse = new GetLastTransactionResponse(m); | ||
if (!gtlResponse.WasRetrievedSuccessfully()) | ||
{ | ||
if (gtlResponse.WasOperationInProgressError()) | ||
if (gtlResponse.IsStillInProgress(txState.PosRefId)) | ||
{ | ||
// TH-4E - Operation In Progress | ||
this._log.info(`Operation still in progress... stay waiting.`); | ||
if (gtlResponse.IsWaitingForSignatureResponse() && !txState.AwaitingSignatureCheck) | ||
{ | ||
this._log.info("Eftpos is waiting for us to send it signature accept/decline, but we were not aware of this. " + | ||
"The user can only really decline at this stage as there is no receipt to print for signing."); | ||
this.CurrentTxFlowState.SignatureRequired(new SignatureRequired(txState.PosRefId, m.Id, "MISSING RECEIPT\n DECLINE AND TRY AGAIN."), "Recovered in Signature Required but we don't have receipt. You may Decline then Retry."); | ||
} | ||
else if (gtlResponse.IsWaitingForAuthCode() && !txState.AwaitingPhoneForAuth) | ||
{ | ||
this._log.info("Eftpos is waiting for us to send it auth code, but we were not aware of this. " + | ||
"We can only cancel the transaction at this stage as we don't have enough information to recover from this."); | ||
this.CurrentTxFlowState.PhoneForAuthRequired(new PhoneForAuthRequired(txState.PosRefId, m.Id, "UNKNOWN", "UNKNOWN"), "Recovered mid Phone-For-Auth but don't have details. You may Cancel then Retry."); | ||
} | ||
else | ||
{ | ||
this._log.info("Operation still in progress... stay waiting."); | ||
// No need to publish txFlowStateChanged. Can return; | ||
return; | ||
} | ||
} | ||
else | ||
{ | ||
// TH-4X - Unexpected Error when recovering | ||
this._log.info(`Unexpected error in Get Last Transaction Response during Transaction Recovery: ${m.GetError()}`); | ||
txState.UnknownCompleted(`Unexpected Error when recovering Transaction Status. Check EFTPOS.`); | ||
// TH-4X - Unexpected Response when recovering | ||
this._log.info(`Unexpected Response in Get Last Transaction during - Received posRefId:${gtlResponse.GetPosRefId()} Error:${m.GetError()}`); | ||
txState.UnknownCompleted("Unexpected Error when recovering Transaction Status. Check EFTPOS. "); | ||
} | ||
@@ -723,3 +1016,3 @@ } | ||
{ | ||
if (txState.Type === TransactionType.GetLastTx) | ||
if (txState.Type == TransactionType.GetLastTransaction) | ||
{ | ||
@@ -730,10 +1023,11 @@ // THIS WAS A PLAIN GET LAST TRANSACTION REQUEST, NOT FOR RECOVERY PURPOSES. | ||
txState.Completed(m.GetSuccessState(), m, "Last Transaction Retrieved"); | ||
} | ||
else | ||
} | ||
else | ||
{ | ||
let successState = this.GltMatch(gtlResponse, txState.Type, txState.AmountCents, txState.RequestTime, "_NOT_IMPL_YET"); | ||
if (successState == SuccessState.Unknown) | ||
// TH-4A - Let's try to match the received last transaction against the current transaction | ||
var successState = this.GltMatch(gtlResponse, txState.PosRefId); | ||
if (successState == SuccessState.Unknown) | ||
{ | ||
// TH-4N: Didn't Match our transaction. Consider Unknown State. | ||
this._log.info(`Did not match transaction.`); | ||
this._log.info("Did not match transaction."); | ||
txState.UnknownCompleted("Failed to recover Transaction Status. Check EFTPOS. "); | ||
@@ -745,5 +1039,5 @@ } | ||
gtlResponse.CopyMerchantReceiptToCustomerReceipt(); | ||
txState.Completed(m.GetSuccessState(), m, "GLT Transaction Ended."); | ||
txState.Completed(successState, m, "Transaction Ended."); | ||
} | ||
} | ||
} | ||
} | ||
@@ -784,2 +1078,6 @@ document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: txState})); | ||
// endregion | ||
// region Internals for Connection Management | ||
_resetConn() | ||
@@ -793,14 +1091,14 @@ { | ||
document.addEventListener('ConnectionStatusChanged', (e) => this._onSpiConnectionStatusChanged(e.detail)); | ||
document.addEventListener('MessageReceived', (e) => this._onSpiMessageReceived(e.detail.data)); | ||
document.addEventListener('MessageReceived', (e) => this._onSpiMessageReceived(e.detail)); | ||
document.addEventListener('ErrorReceived', (e) => this._onWsErrorReceived(e.detail)); | ||
} | ||
/// <summary> | ||
/// This method will be called when the connection status changes. | ||
/// You are encouraged to display a PinPad Connection Indicator on the POS screen. | ||
/// </summary> | ||
/// <param name="state"></param> | ||
// <summary> | ||
// This method will be called when the connection status changes. | ||
// You are encouraged to display a PinPad Connection Indicator on the POS screen. | ||
// </summary> | ||
// <param name="state"></param> | ||
_onSpiConnectionStatusChanged(state) | ||
{ | ||
switch (state) | ||
switch (state.ConnectionState) | ||
{ | ||
@@ -833,4 +1131,2 @@ case ConnectionState.Connecting: | ||
this._missedPongsCount = 0; | ||
this._mostRecentLoginResponse = null; | ||
this._readyToTransact = false; | ||
this._stopPeriodicPing(); | ||
@@ -849,4 +1145,4 @@ | ||
if (this._conn == null) return; // This means the instance has been disposed. Aborting. | ||
this._log.info(`Will try to reconnect in 5s...`); | ||
setTimeout(() => { | ||
@@ -873,2 +1169,7 @@ if (this.CurrentStatus != SpiStatus.Unpaired) | ||
// <summary> | ||
// This is an important piece of the puzzle. It's a background thread that periodically | ||
// sends Pings to the server. If it doesn't receive Pongs, it considers the connection as broken | ||
// so it disconnects. | ||
// </summary> | ||
_startPeriodicPing() { | ||
@@ -905,3 +1206,2 @@ this._stopPeriodicPing(); | ||
this._conn.Disconnect(); | ||
this._readyToTransact = false; | ||
this._stopPeriodicPing(); | ||
@@ -920,8 +1220,10 @@ } | ||
/// <summary> | ||
/// We call this ourselves as soon as we're ready to transact with the PinPad after a connection is established. | ||
/// This function is effectively called after we received the first Login Response from the PinPad. | ||
/// </summary> | ||
// <summary> | ||
// We call this ourselves as soon as we're ready to transact with the PinPad after a connection is established. | ||
// This function is effectively called after we received the first Login Response from the PinPad. | ||
// </summary> | ||
_onReadyToTransact() | ||
{ | ||
this._log.info("On Ready To Transact!"); | ||
// So, we have just made a connection, pinged and logged in successfully. | ||
@@ -943,11 +1245,18 @@ this.CurrentStatus = SpiStatus.PairedConnected; | ||
this._send(this.CurrentTxFlowState.Request); | ||
this.CurrentTxFlowState.Sent(`Asked EFTPOS to accept payment for ${this.CurrentTxFlowState.AmountCents / 100.0}`); | ||
this.CurrentTxFlowState.Sent(`Sending Request Now...`); | ||
document.dispatchEvent(new CustomEvent('TxFlowStateChanged', {detail: this.CurrentTxFlowState})); | ||
} | ||
} | ||
else | ||
{ | ||
// let's also tell the eftpos our latest table configuration. | ||
if(this._spiPat) { | ||
this._spiPat.PushPayAtTableConfig(); | ||
} | ||
} | ||
} | ||
/// <summary> | ||
/// When we disconnect, we should also stop the periodic ping. | ||
/// </summary> | ||
// <summary> | ||
// When we disconnect, we should also stop the periodic ping. | ||
// </summary> | ||
_stopPeriodicPing() { | ||
@@ -961,3 +1270,2 @@ if(this._periodicPingThread) { | ||
// Send a Ping to the Server | ||
@@ -969,8 +1277,9 @@ _doPing() | ||
this._send(ping); | ||
this._mostRecentPingSentTime = Date.now(); | ||
} | ||
/// <summary> | ||
/// Received a Pong from the server | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// Received a Pong from the server | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleIncomingPong(m) | ||
@@ -981,57 +1290,24 @@ { | ||
if (this._mostRecentLoginResponse == null || | ||
this._mostRecentLoginResponse.ExpiringSoon(this._spiMessageStamp.ServerTimeDelta)) | ||
if (this._mostRecentPongReceived == null) | ||
{ | ||
// We have not logged in yet, or login expiring soon. | ||
this._doLogin(); | ||
} | ||
this._mostRecentPongReceived = m; | ||
} | ||
/// <summary> | ||
/// Login is a mute thing but is required. | ||
/// </summary> | ||
_doLogin() | ||
{ | ||
var lr = LoginHelper.NewLoginRequest(); | ||
this._send(lr.ToMessage()); | ||
} | ||
/// <summary> | ||
/// When the server replied to our LoginRequest with a LoginResponse, we take note of it. | ||
/// </summary> | ||
/// <param name="m"></param> | ||
_handleLoginResponse(m) | ||
{ | ||
var lr = new LoginResponse(m); | ||
if (lr.Success) | ||
{ | ||
this._mostRecentLoginResponse = lr; | ||
if (!this._readyToTransact) | ||
// First pong received after a connection, and after the pairing process is fully finalised. | ||
if (this.CurrentStatus != SpiStatus.Unpaired) | ||
{ | ||
// We are finally ready to make transactions. | ||
// Let's notify ourselves so we can take some actions. | ||
this._readyToTransact = true; | ||
this._log.info(`Logged in Successfully. Expires: ${lr.Expires}`); | ||
if (this.CurrentStatus != SpiStatus.Unpaired) { | ||
this._onReadyToTransact(); | ||
} | ||
this._log.info("First pong of connection and in paired state."); | ||
this._onReadyToTransact(); | ||
} | ||
else | ||
{ | ||
this._log.info(`I have just refreshed my Login. Now Expires: ${lr.Expires}`); | ||
this._log.info("First pong of connection but pairing process not finalised yet."); | ||
} | ||
} | ||
else | ||
{ | ||
this._log.info("Logged in Failure."); | ||
this._conn.Disconnect(); | ||
} | ||
this._mostRecentPongReceived = m; | ||
this._log.debug(`PongLatency:${Date.now() - this._mostRecentPingSentTime}`); | ||
} | ||
/// <summary> | ||
/// The server will also send us pings. We need to reply with a pong so it doesn't disconnect us. | ||
/// </summary> | ||
/// <param name="m"></param> | ||
// <summary> | ||
// The server will also send us pings. We need to reply with a pong so it doesn't disconnect us. | ||
// </summary> | ||
// <param name="m"></param> | ||
_handleIncomingPing(m) | ||
@@ -1043,5 +1319,5 @@ { | ||
/// <summary> | ||
/// Ask the PinPad to tell us what the Most Recent Transaction was | ||
/// </summary> | ||
// <summary> | ||
// Ask the PinPad to tell us what the Most Recent Transaction was | ||
// </summary> | ||
_callGetLastTransaction() | ||
@@ -1053,12 +1329,18 @@ { | ||
/// <summary> | ||
/// This method will be called whenever we receive a message from the Connection | ||
/// </summary> | ||
/// <param name="messageJson"></param> | ||
// <summary> | ||
// This method will be called whenever we receive a message from the Connection | ||
// </summary> | ||
// <param name="messageJson"></param> | ||
_onSpiMessageReceived(messageJson) | ||
{ | ||
// First we parse the incoming message | ||
var m = Message.FromJson(messageJson, this._secrets); | ||
var m = Message.FromJson(messageJson.Message, this._secrets); | ||
this._log.info("Received:" + m.DecryptedJson); | ||
if (SpiPreauth.IsPreauthEvent(m.EventName)) | ||
{ | ||
this._spiPreauth._handlePreauthMessage(m); | ||
return; | ||
} | ||
// And then we switch on the event type. | ||
@@ -1076,4 +1358,4 @@ switch (m.EventName) | ||
break; | ||
case Events.LoginResponse: | ||
this._handleLoginResponse(m); | ||
case Events.DropKeysAdvice: | ||
this._handleDropKeysAdvice(m); | ||
break; | ||
@@ -1086,5 +1368,14 @@ case Events.PurchaseResponse: | ||
break; | ||
case Events.CashoutOnlyResponse: | ||
this._handleCashoutOnlyResponse(m); | ||
break; | ||
case Events.MotoPurchaseResponse: | ||
this._handleMotoPurchaseResponse(m); | ||
break; | ||
case Events.SignatureRequired: | ||
this._handleSignatureRequired(m); | ||
break; | ||
case Events.AuthCodeRequired: | ||
this._handleAuthCodeRequired(m); | ||
break; | ||
case Events.GetLastTransactionResponse: | ||
@@ -1096,2 +1387,5 @@ this._handleGetLastTransactionResponse(m); | ||
break; | ||
case Events.SettlementEnquiryResponse: | ||
this._handleSettlementEnquiryResponse(m); | ||
break; | ||
case Events.Ping: | ||
@@ -1106,2 +1400,16 @@ this._handleIncomingPing(m); | ||
break; | ||
case Events.PayAtTableGetTableConfig: | ||
if (this._spiPat == null) | ||
{ | ||
this._send(PayAtTableConfig.FeatureDisableMessage(RequestIdHelper.Id("patconf"))); | ||
break; | ||
} | ||
this._spiPat._handleGetTableConfig(m); | ||
break; | ||
case Events.PayAtTableGetBillDetails: | ||
this._spiPat._handleGetBillDetailsRequest(m); | ||
break; | ||
case Events.PayAtTableBillPayment: | ||
this._spiPat._handleBillPaymentAdvice(m); | ||
break; | ||
case Events.Error: | ||
@@ -1121,3 +1429,3 @@ this._handleErrorEvent(m); | ||
{ | ||
this._log.warn("Received WS Error: " + error); | ||
this._log.warn("Received WS Error: " + error.Message); | ||
} | ||
@@ -1124,0 +1432,0 @@ |
@@ -1,82 +0,82 @@ | ||
/// <summary> | ||
/// Represents the 3 Pairing statuses that the Spi instanxce can be in. | ||
/// </summary> | ||
// <summary> | ||
// Represents the 3 Pairing statuses that the Spi instanxce can be in. | ||
// </summary> | ||
const SpiStatus = | ||
{ | ||
/// <summary> | ||
/// Paired and Connected | ||
/// </summary> | ||
// <summary> | ||
// Paired and Connected | ||
// </summary> | ||
PairedConnected: 'PairedConnected', | ||
/// <summary> | ||
/// Paired but trying to establish a connection | ||
/// </summary> | ||
// <summary> | ||
// Paired but trying to establish a connection | ||
// </summary> | ||
PairedConnecting: 'PairedConnecting', | ||
/// <summary> | ||
/// Unpaired | ||
/// </summary> | ||
// <summary> | ||
// Unpaired | ||
// </summary> | ||
Unpaired: 'Unpaired' | ||
}; | ||
/// <summary> | ||
/// The Spi instance can be in one of these flows at any point in time. | ||
/// </summary> | ||
// <summary> | ||
// The Spi instance can be in one of these flows at any point in time. | ||
// </summary> | ||
const SpiFlow = | ||
{ | ||
/// <summary> | ||
/// Currently going through the Pairing Process Flow. | ||
/// Happens during the Unpaired SpiStatus. | ||
/// </summary> | ||
// <summary> | ||
// Currently going through the Pairing Process Flow. | ||
// Happens during the Unpaired SpiStatus. | ||
// </summary> | ||
Pairing: 'Pairing', | ||
/// <summary> | ||
/// Currently going through the transaction Process Flow. | ||
/// Cannot happen in the Unpaired SpiStatus. | ||
/// </summary> | ||
// <summary> | ||
// Currently going through the transaction Process Flow. | ||
// Cannot happen in the Unpaired SpiStatus. | ||
// </summary> | ||
Transaction: 'Transaction', | ||
/// <summary> | ||
/// Not in any of the other states. | ||
/// </summary> | ||
// <summary> | ||
// Not in any of the other states. | ||
// </summary> | ||
Idle: 'Idle' | ||
}; | ||
/// <summary> | ||
/// Represents the Pairing Flow State during the pairing process | ||
/// </summary> | ||
// <summary> | ||
// Represents the Pairing Flow State during the pairing process | ||
// </summary> | ||
class PairingFlowState | ||
{ | ||
constructor(state) { | ||
/// <summary> | ||
/// Some text that can be displayed in the Pairing Process Screen | ||
/// that indicates what the pairing process is up to. | ||
/// </summary> | ||
// <summary> | ||
// Some text that can be displayed in the Pairing Process Screen | ||
// that indicates what the pairing process is up to. | ||
// </summary> | ||
this.Message = null; | ||
/// <summary> | ||
/// When true, it means that the EFTPOS is shoing the confirmation code, | ||
/// and your user needs to press YES or NO on the EFTPOS. | ||
/// </summary> | ||
// <summary> | ||
// When true, it means that the EFTPOS is shoing the confirmation code, | ||
// and your user needs to press YES or NO on the EFTPOS. | ||
// </summary> | ||
this.AwaitingCheckFromEftpos = null; | ||
/// <summary> | ||
/// When true, you need to display the YES/NO buttons on you pairing screen | ||
/// for your user to confirm the code. | ||
/// </summary> | ||
// <summary> | ||
// When true, you need to display the YES/NO buttons on you pairing screen | ||
// for your user to confirm the code. | ||
// </summary> | ||
this.AwaitingCheckFromPos = null; | ||
/// <summary> | ||
/// This is the confirmation code for the pairing process. | ||
/// </summary> | ||
// <summary> | ||
// This is the confirmation code for the pairing process. | ||
// </summary> | ||
this.ConfirmationCode = null; | ||
/// <summary> | ||
/// Indicates whether the Pairing Flow has finished its job. | ||
/// </summary> | ||
// <summary> | ||
// Indicates whether the Pairing Flow has finished its job. | ||
// </summary> | ||
this.Finished = null; | ||
/// <summary> | ||
/// Indicates whether pairing was successful or not. | ||
/// </summary> | ||
// <summary> | ||
// Indicates whether pairing was successful or not. | ||
// </summary> | ||
this.Successful = null; | ||
@@ -94,10 +94,16 @@ | ||
Refund: 'Refund', | ||
CashoutOnly: 'CashoutOnly', | ||
MOTO: 'MOTO', | ||
Settle: 'Settle', | ||
GetLastTx: 'GetLastTx' | ||
SettlementEnquiry: 'SettlementEnquiry', | ||
GetLastTransaction: 'GetLastTransaction', | ||
Preauth: 'Preauth', | ||
AccountVerify: 'AccountVerify' | ||
}; | ||
/// <summary> | ||
/// Used as a return in the InitiateTx methods to signify whether | ||
/// the transaction was initiated or not, and a reason to go with it. | ||
/// </summary> | ||
// <summary> | ||
// Used as a return in the InitiateTx methods to signify whether | ||
// the transaction was initiated or not, and a reason to go with it. | ||
// </summary> | ||
class InitiateTxResult | ||
@@ -107,12 +113,12 @@ { | ||
{ | ||
/// <summary> | ||
/// Whether the tx was initiated. | ||
/// When true, you can expect updated to your registered callback. | ||
/// When false, you can retry calling the InitiateX method. | ||
/// </summary> | ||
// <summary> | ||
// Whether the tx was initiated. | ||
// When true, you can expect updated to your registered callback. | ||
// When false, you can retry calling the InitiateX method. | ||
// </summary> | ||
this.Initiated = initiated; | ||
/// <summary> | ||
/// Text that gives reason for the Initiated flag, especially in case of false. | ||
/// </summary> | ||
// <summary> | ||
// Text that gives reason for the Initiated flag, especially in case of false. | ||
// </summary> | ||
this.Message = message; | ||
@@ -122,94 +128,123 @@ } | ||
/// <summary> | ||
/// Represents the State during a TransactionFlow | ||
/// </summary> | ||
// <summary> | ||
// Used as a return in calls mid transaction to let you know | ||
// whether the call was valid or not. | ||
// These attributes work for COM interop. | ||
// </summary> | ||
class MidTxResult | ||
{ | ||
// <summary> | ||
// This default stucture works for COM interop. | ||
// </summary> | ||
constructor(valid, message) | ||
{ | ||
this.Valid = valid; | ||
this.Message = message; | ||
} | ||
} | ||
// <summary> | ||
// Represents the State during a TransactionFlow | ||
// </summary> | ||
class TransactionFlowState | ||
{ | ||
constructor(id, type, amountCents, message, msg) | ||
constructor(posRefId, type, amountCents, message, msg) | ||
{ | ||
/// <summary> | ||
/// The id given to this transaction | ||
/// </summary> | ||
this.Id = id; | ||
// <summary> | ||
// The id given to this transaction | ||
// </summary> | ||
this.PosRefId = posRefId; | ||
this.Id = posRefId; // obsolete, but let's maintain it for now, to mean same as PosRefId. | ||
/// <summary> | ||
/// Purchase/Refund/Settle/... | ||
/// </summary> | ||
// <summary> | ||
// Purchase/Refund/Settle/... | ||
// </summary> | ||
this.Type = type; | ||
/// <summary> | ||
/// Amount in cents for this transaction | ||
/// </summary> | ||
// <summary> | ||
// A text message to display on your Transaction Flow Screen | ||
// </summary> | ||
this.DisplayMessage = msg; | ||
// <summary> | ||
// Amount in cents for this transaction | ||
// </summary> | ||
this.AmountCents = amountCents; | ||
/// <summary> | ||
/// Whther the request has been sent to the EFTPOS yet or not. | ||
/// In the PairedConnecting state, the transaction is initiated | ||
/// but the request is only sent once the connection is recovered. | ||
/// </summary> | ||
// <summary> | ||
// Whther the request has been sent to the EFTPOS yet or not. | ||
// In the PairedConnecting state, the transaction is initiated | ||
// but the request is only sent once the connection is recovered. | ||
// </summary> | ||
this.RequestSent = false; | ||
/// <summary> | ||
/// The time when the request was sent to the EFTPOS. | ||
/// </summary> | ||
// <summary> | ||
// The time when the request was sent to the EFTPOS. | ||
// </summary> | ||
this.RequestTime = null; | ||
/// <summary> | ||
/// The time when we last asked for an update, including the original request at first | ||
/// </summary> | ||
// <summary> | ||
// The time when we last asked for an update, including the original request at first | ||
// </summary> | ||
this.LastStateRequestTime = null; | ||
/// <summary> | ||
/// Whether we're currently attempting to Cancel the transaction. | ||
/// </summary> | ||
// <summary> | ||
// Whether we're currently attempting to Cancel the transaction. | ||
// </summary> | ||
this.AttemptingToCancel = null; | ||
/// <summary> | ||
/// When this flag is on, you need to display the dignature accept/decline buttons in your | ||
/// transaction flow screen. | ||
/// </summary> | ||
// <summary> | ||
// When this flag is on, you need to display the dignature accept/decline buttons in your | ||
// transaction flow screen. | ||
// </summary> | ||
this.AwaitingSignatureCheck = false; | ||
/// <summary> | ||
/// Whether this transaction flow is over or not. | ||
/// </summary> | ||
// <summary> | ||
// When this flag is on, you need to show your user the phone number to call to get the authorisation code. | ||
// Then you need to provide your user means to enter that given code and submit it via SubmitAuthCode(). | ||
// </summary> | ||
this.AwaitingPhoneForAuth = null; | ||
// <summary> | ||
// Whether this transaction flow is over or not. | ||
// </summary> | ||
this.Finished = false; | ||
/// <summary> | ||
/// The success state of this transaction. Starts off as Unknown. | ||
/// When finished, can be Success, Failed OR Unknown. | ||
/// </summary> | ||
// <summary> | ||
// The success state of this transaction. Starts off as Unknown. | ||
// When finished, can be Success, Failed OR Unknown. | ||
// </summary> | ||
this.Success = SuccessState.Unknown; | ||
/// <summary> | ||
/// The request message that we are sending/sent to the server. | ||
/// </summary> | ||
this.Request = message; | ||
/// <summary> | ||
/// The response at the end of the transaction. | ||
/// Might not be present in all edge cases. | ||
/// You can then turn this Message into the appropriate structure, | ||
/// such as PurchaseResponse, RefundResponse, etc | ||
/// </summary> | ||
// <summary> | ||
// The response at the end of the transaction. | ||
// Might not be present in all edge cases. | ||
// You can then turn this Message into the appropriate structure, | ||
// such as PurchaseResponse, RefundResponse, etc | ||
// </summary> | ||
this.Response = null; | ||
/// <summary> | ||
/// A text message to display on your Transaction Flow Screen | ||
/// </summary> | ||
this.DisplayMessage = msg; | ||
/// <summary> | ||
/// The message the we received from EFTPOS that told us that signature is required. | ||
/// </summary> | ||
// <summary> | ||
// The message the we received from EFTPOS that told us that signature is required. | ||
// </summary> | ||
this.SignatureRequiredMessage = null; | ||
/// <summary> | ||
/// The time when the cancel attempt was made. | ||
/// </summary> | ||
// <summary> | ||
// The message the we received from EFTPOS that told us that Phone For Auth is required. | ||
// </summary> | ||
this.PhoneForAuthRequiredMessage = null; | ||
// <summary> | ||
// The time when the cancel attempt was made. | ||
// </summary> | ||
this.CancelAttemptTime = null; | ||
/// <summary> | ||
/// Whether we're currently waiting for a Get Last Transaction Response to get an update. | ||
/// </summary> | ||
// <summary> | ||
// The request message that we are sending/sent to the server. | ||
// </summary> | ||
this.Request = message; | ||
// <summary> | ||
// Whether we're currently waiting for a Get Last Transaction Response to get an update. | ||
// </summary> | ||
this.AwaitingGltResponse = null; | ||
@@ -265,2 +300,15 @@ } | ||
PhoneForAuthRequired(spiMessage, msg) | ||
{ | ||
this.PhoneForAuthRequiredMessage = spiMessage; | ||
this.AwaitingPhoneForAuth = true; | ||
this.DisplayMessage = msg; | ||
} | ||
AuthCodeSent(msg) | ||
{ | ||
this.AwaitingPhoneForAuth = false; | ||
this.DisplayMessage = msg; | ||
} | ||
Completed(state, response, msg) | ||
@@ -274,2 +322,3 @@ { | ||
this.AwaitingSignatureCheck = false; | ||
this.AwaitingPhoneForAuth = false; | ||
this.DisplayMessage = msg; | ||
@@ -286,4 +335,48 @@ } | ||
this.AwaitingSignatureCheck = false; | ||
this.AwaitingPhoneForAuth = false; | ||
this.DisplayMessage = msg; | ||
} | ||
} | ||
// <summary> | ||
// Used as a return in the SubmitAuthCode method to signify whether Code is valid | ||
// </summary> | ||
class SubmitAuthCodeResult | ||
{ | ||
constructor(validFormat, message) | ||
{ | ||
this.ValidFormat = validFormat; | ||
// <summary> | ||
// Text that gives reason for Invalidity | ||
// </summary> | ||
this.Message = message; | ||
} | ||
} | ||
class SpiConfig | ||
{ | ||
constructor() { | ||
this.PromptForCustomerCopyOnEftpos = false; | ||
this.SignatureFlowOnEftpos = false; | ||
} | ||
addReceiptConfig(messageData) | ||
{ | ||
if (this.PromptForCustomerCopyOnEftpos) | ||
{ | ||
messageData.prompt_for_customer_copy = this.PromptForCustomerCopyOnEftpos; | ||
} | ||
if (this.SignatureFlowOnEftpos) | ||
{ | ||
messageData.print_for_signature_required_transactions = this.SignatureFlowOnEftpos; | ||
} | ||
return messageData; | ||
} | ||
ToString() | ||
{ | ||
return `PromptForCustomerCopyOnEftpos:${this.PromptForCustomerCopyOnEftpos} SignatureFlowOnEftpos:${this.SignatureFlowOnEftpos}`; | ||
} | ||
} |
describe('MessagesTest', function() { | ||
'use strict'; | ||
const mockKeyRequest = JSON.stringify({ | ||
message: { | ||
event: "key_request", | ||
id: "62", | ||
data: { | ||
enc: { | ||
A: "17E7BE43D53102647040FC090000C215810E28E5E0CBD4F47923E194AE72AB0CDADF922642B73C568AA94A84B61874A475549E1F95847BE2725462E3D635F019BE39B2064F1EFFBE6B80CE97FBB7C0913ADC06A2445980B57647778B127FFCCE8B28A44BADEDE0110A5AFB05FEF7AA3F54988AFB04310A113F713601683D8E30CA2BAFC2EC34879127019E3352D8CAB9603184283AE3C9359D40C12474500018B8640AF371DC8712A06A3A443DF41DA9C1C60FAD2ACB02564A6694382B18811AA30CE38A1FC251DE0669504CAB620C2BA4A84CCC8FBDCBB30BBB3EACA76008599F74C2FDF6231773DC0439969CB5F2904A71DDF57F7DF9394AA29CBE4856FC82" | ||
}, | ||
hmac: { | ||
A: "89708531EADF129B4F67F00ECBF883C825A0EF3D766E32BC2BA13508B53FC3F5928316DE05CBE82FA1BBF4116E58A68C6F9C3C8FEF492051498188F4E80F82D5764FF50331B34E418E41480FAE0C794F20D9F7AE9819CB317AD2351B165783D57D12C39F95D9A5A292B89D3A26F9BBDE5C218EEC3FE63D910DCB0E1A0E6B570AF94BBD3025EB5E23FFBD9E8D58FE68403B3E50566DA8E2E54EED1A4D754689ECB7266B3D4804E39FB868F1741896757E7844C3389DA49F87D23FB2E9F6ADDBE9C14CC92F322CF3B471CE217E48D0762D5C963827AA6F4316B905F19E0262A35DC4B62E2FB95B7AAD5616C61F31C9A74008EE51BAB2CD6F646320FA30A6DDC4D7" | ||
} | ||
} | ||
} | ||
}); | ||
const mockEncryptedMsg = JSON.stringify({ | ||
@@ -40,2 +25,3 @@ enc: "819A6FF34A7656DBE5274AC44A28A48DD6D723FCEF12570E4488410B83A1504084D79BA9DF05C3CE58B330C6626EA5E9EB6BAAB3BFE95345A8E9834F183A1AB2F6158E8CDC217B4970E6331B4BE0FCAA", | ||
var mockKeyRequest = JSON.stringify(__fixtures__['KeyRequest']); | ||
var incomingMessage = Message.FromJson(mockKeyRequest, null); | ||
@@ -42,0 +28,0 @@ |
describe('PairingTests', function() { | ||
'use strict'; | ||
const mockKeyRequest = JSON.stringify({ | ||
message: { | ||
event: "key_request", | ||
id: "62", | ||
data: { | ||
enc: { | ||
A: "17E7BE43D53102647040FC090000C215810E28E5E0CBD4F47923E194AE72AB0CDADF922642B73C568AA94A84B61874A475549E1F95847BE2725462E3D635F019BE39B2064F1EFFBE6B80CE97FBB7C0913ADC06A2445980B57647778B127FFCCE8B28A44BADEDE0110A5AFB05FEF7AA3F54988AFB04310A113F713601683D8E30CA2BAFC2EC34879127019E3352D8CAB9603184283AE3C9359D40C12474500018B8640AF371DC8712A06A3A443DF41DA9C1C60FAD2ACB02564A6694382B18811AA30CE38A1FC251DE0669504CAB620C2BA4A84CCC8FBDCBB30BBB3EACA76008599F74C2FDF6231773DC0439969CB5F2904A71DDF57F7DF9394AA29CBE4856FC82" | ||
}, | ||
hmac: { | ||
A: "89708531EADF129B4F67F00ECBF883C825A0EF3D766E32BC2BA13508B53FC3F5928316DE05CBE82FA1BBF4116E58A68C6F9C3C8FEF492051498188F4E80F82D5764FF50331B34E418E41480FAE0C794F20D9F7AE9819CB317AD2351B165783D57D12C39F95D9A5A292B89D3A26F9BBDE5C218EEC3FE63D910DCB0E1A0E6B570AF94BBD3025EB5E23FFBD9E8D58FE68403B3E50566DA8E2E54EED1A4D754689ECB7266B3D4804E39FB868F1741896757E7844C3389DA49F87D23FB2E9F6ADDBE9C14CC92F322CF3B471CE217E48D0762D5C963827AA6F4316B905F19E0262A35DC4B62E2FB95B7AAD5616C61F31C9A74008EE51BAB2CD6F646320FA30A6DDC4D7" | ||
} | ||
} | ||
} | ||
}); | ||
@@ -22,2 +7,3 @@ it('TestPairingKeyResponse', function() { | ||
var secrets = null; | ||
var mockKeyRequest = JSON.stringify(__fixtures__['KeyRequest']); | ||
var incomingMessage = Message.FromJson(mockKeyRequest, secrets); | ||
@@ -24,0 +10,0 @@ |
@@ -9,6 +9,7 @@ describe('Purchase', function() { | ||
var purchase = PurchaseHelper.CreatePurchaseRequest(purchaseAmountCents, RequestIdHelper.Id("p")); | ||
var id = RequestIdHelper.Id("p"); | ||
var purchase = PurchaseHelper.CreatePurchaseRequest(purchaseAmountCents, id); | ||
expect(purchase.AmountCents).toBe(10); | ||
expect(purchase.Id).toBe('p1'); | ||
expect(purchase.Id).toBe(id); | ||
@@ -22,5 +23,5 @@ var encryptedMsg = purchase.ToMessage().ToJson(spiMessageStamp); | ||
expect(decryptedMsg.Id).toBe('p1'); | ||
expect(decryptedMsg.Id.substr(0,5)).toBe('prchs'); | ||
expect(decryptedMsg.EventName).toBe(Events.PurchaseRequest); | ||
expect(decryptedMsg.Data.amount_purchase).toBe(purchaseAmountCents); | ||
expect(decryptedMsg.Data.purchase_amount).toBe(purchaseAmountCents); | ||
@@ -27,0 +28,0 @@ }); |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
369852
47
8257
8