Web retail application συνέχεια
Και αντί να πηγαίνουμε στο supermarket για να ψωνίσουμε μπορούμε να ψωνίζουμε από το σπίτι μας με παραγγελίες μέσω του internet. Αντί για barcode scanner επιλέγουμε τα είδη μέσω εικόνων. Πληρώνουμε με πιστωτική κάρτα, εισάγοντας τα στοιχεία μας στο ασφαλές site της τράπεζας και ο έμπορος λαμβάνει απλά την επιβεβαίωση της πληρωμής.
Η αρχιτεκτονική του λογισμικού παραμένει η ίδια. Με ελάχιστη προσπάθεια οι 2 λύσεις θα χρησιμοποιούν τον ίδιο server. Και οι κερδισμένοι πόντοι, μαζί με άλλα προωθητικά μηνύματα θα καταφθάνουν με sms. Η μεγαλύτερη πλέον Ελληνική εταιρεία, με γαλλικό άρωμα, στρέφεται στο internet σαν νέο τρόπο αγορών για το supermarket. Μία κεντρική αποθήκη θα εξυπηρετεί τις παραγγελίες, τα καταστήματα θα μικρύνουν (λιγότεροι πελάτες) και τα κόστη θα μειωθούν. Και βέβαια όσο περισσότερο μένουμε στο σπίτι τόσο καλύτερα.[V]
Τράπεζα έκδοσης της κάρτας (issuer) και τράπεζα του EFT (aquirer).
Την κάρτα μας και ο συνδεδεμένος με αυτή λογαριασμός μας τηρείται σε μία τράπεζα Α. Ο αριθμός της κάρτας (15 ψηφία + 1 ψηφίο LUHN check digit ) παραπέμπουν στην τράπεζα έκδοσης. Το τερματικό, στην γενικότητά του, ανήκει σε κάποια άλλη τράπεζα Β διαφορετική από τον εκδότη της κάρτας που χρησιμοποιούμε. Το τερματικό θα εξυπηρετήσει απευθείας τις κάρτες της ίδιας τράπεζας Β και θα στείλει όλες τις ξένες συναλλαγές στην VISA, MC, ΑΜΕΧ κτλ. Αυτές με την σειρά του θα τις προωθήσουν στην εκδότρια τράπεζα Α, που ξέρει το υπόλοιπο του λογαριασμού μας και η απάντηση θα ακολουθήσει την αντίθετη διαδικασία.
Ανάλυση & σχεδίαση ενός ιδεατού (virtual) ISO8583 POS.
Ένα άλλο διαδεδομένο τραπεζικό πρωτόκολλο είναι το
SPDH.
Πως προχωράμε ?. Δεν έχουμε σύνοψη για το SPDH. Θα το αγνοήσουμε ή θα λάβουμε στοιχειώδη πρόνοια ώστε να είναι εύκολη η πλήρης υποστήριξή του στην συνέχεια ?
Επιλέγω να το υποστηρίξω. Ξεχνάω τις ιδιαιτερότητες του ISO8583 και συγκεντρώνομαι στις λειτουργικές προδιαγραφές. Τι πρέπει να κάνει ένα τερματικό για πιστωτικές κάρτες ? Και τα δύο πρωτόκολλα έχουν σχεδιασθεί για να ικανοποιούν αυτές ακριβώς τις ανάγκες.
Θα ορίσουμε μια ιδεατή (abstract) κλάση ΤBaseEFTPos με μεθόδους τις χρηματοοικονομικές υπηρεσίες (που είναι ανεξάρτητες του πρωτοκόλλου) και στην συνέχεια τα 2 παιδιά της ΤSPDHEFT και TISO8583EFT που υλοποιούν αυτές τις υπηρεσίες με την χρήση κάθε πρωτοκόλλου.
Η ΤBaseEFTPos έχει μεθόδους
Αρχικοποίηση - Initialization
Προέγκριση - Αuthorization (στοιχεία κάρτας, ποσό) σε Ξενοδοχεία, ενοικιάσεις αυτοκινήτων
Πώληση - Sale(στοιχεία κάρτας, ποσό)
Ακύρωση Πώλησης - Void Sale (στοιχεία κάρτας, ποσό)
Επιστροφή - Refund (στοιχεία κάρτας, ποσό) Σε άλλη χρονική στιγμή
Ακύρωση Επιστροφή – Void Refund(στοιχεία κάρτας, ποσό)
Οριστικοποίηση - Finalization
Η κλάση TISO8583EFT πρέπει
A. να υλοποιεί τις μεθόδους του TBaseEFT
B. να μπορεί να ανακαλεί και να σώζει τα στοιχεία των συναλλαγών σε μια βάση δεδομένων
Γ. να μορφοποιεί και να αναλύει ISO8583 μηνύματα
Δ. να ανταλλάσει, μέσω tcp/ip, ISO8583 μηνύματα με την τράπεζα.
Ε. να ελέγχει και να υλοποιεί τους αντιλογισμούς (reversals)
Ζ. να τηρεί και να κλείνει το batch
Η. να ξαναστέλνει το batch
Η κλάση TSPDHEFT πρέπει
Α. να υλοποιεί τις μεθόδους του TBaseEFT
Β. να μπορεί να ανακαλεί και να σώζει τα στοιχεία των συναλλαγών σε μια βάση δεδομένων
Γ. να μορφοποιεί και να αναλύει SPDH μηνύματα
Δ. να ανταλλάσει, μέσω x25 SPDH μηνύματα με την τράπεζα.
Ε. να ελέγχει και να υλοποιεί τους αντιλογισμούς (reversals)
Ζ. να τηρεί και να κλείνει το batch
Οι σημαντικότερες διαφορές στα 2 τραπεζικά πρωτόκολλα είναι η λογική του reversal και το κλείσιμο batch. Στο SPDΗ τηρούνται και συγκρίνονται τα σύνολα και σε περίπτωση σφάλματος η διαδικασία σταματάει και απαιτεί ανθρώπινη επέμβαση.
Κλάσεις
Έχουμε ορίσει την ιεραρχία που αποτυπώνεται στο παρακάτω σχήμα.
Ο έξω κόσμος βλέπει τις υπηρεσίες που παρέχει το TBasePos. Αυτές απαιτούν την επιλογή ενός τερματικού, ένα ποσό, μια κάρτα και μία ενέργεια, δλδ, ο έμπορος ολοκλήρωσε μια συναλλαγή πληρωμής στο τερματικό της τράπεζας Α για το ποσό Χ.
Το τερματικό ΔΕΝ έχει απευθείας πρόσβαση στα δεδομένα της βάσης δεδομένων. Έτσι μπορώ να χρησιμοποιήσω flat αρχεία ή oracle ή mysql χωρίς να επηρεάσω την λειτουργικότητα του TBaseEft.
Για να απομονώσω το TBasePos από την βάση δεδομένων δημιουργώ ένα μία ενδιάμεση κλάση dbHandler. Αυτή επικοινωνεί με το TBasePos και ανταλλάσει δεδομένα με χρήση των κλάσεων NeutralTerm και NeutralTrn. Επιπλέον γνωρίζει πώς να τις «γεμίζει» με στοιχεία από την βάση δεδομένων.
Στην βάση δεδομένων δημιουργώ πίνακες για όλες τις οντότητες που χρησιμοποιούμε. Οι αντίστοιχες κλάσεις έχουν σαν attributes τα πεδία της βάσης και κληρονομούν τις μεθόδους τους από την κλάση dbTable.
Μέχρι τώρα έμεινα στην σχεδίαση. Μην γελαστείτε. Δεν έχει σημασία που το software είναι soft. Δεν έχει σημασία που αλλάζει σχετικά εύκολα. Η σχεδίαση είναι το πρώτο βήμα σε κάθε project. Το cash flow είναι το δεύτερο. Η υλοποίηση έπεται.
Δεν αναφέρω κάτι για ασφάλεια και passwords και encryption. Θεωρώ ότι αυτές οι εφαρμογές «ζουν» σε ασφαλή περιβάλλοντα. Επιπλέον δεν αναφέρομαι στα κλειδιά που ανταλλάσσονται για την κωδικοποίηση και αποκωδικοποίηση του PIN. Το θέμα του Chip είναι από μόνο του ένα θηρίο
Μηνύματα τύπου ISO8583.
Ένα μήνυμα αποτελείται από την σύνθεση των επιμέρους πεδίων του. Κάθε μήνυμα έχει μια επικεφαλίδα ένα περιεχόμενο και πιθανά ένα κωδικό επαλήθευσης (crc).
Συνήθως τα πεδία αποθηκεύονται το ένα μετά το άλλο και χωρίζονται με ένα χαρακτήρα (Field Separator).
Όταν όμως έχεις μια απλή τηλεφωνική γραμμή και ένα modem 2400 baud κάθε επιπλέον byte μετράει. Η χρόνος επικοινωνίας είναι ανάλογος με το μέγεθος του μηνύματος.
Πρέπει ο σχεδιαστής να βρει τρόπο ώστε να μεταφέρει μόνο όσα πεδία απαιτούνται την φορά. Έτσι εισάγεται το bitmap. Χρησιμοποιούμε τα bits από 8 συνεχόμενα bytes για να δείξουμε πια πεδία, (1..64) υπάρχουν ή όχι.
Τo ISO8583 μήνυμα αποτελείται από ascii χαρακτήρες και αρχίζει με κάποια σταθερά πεδία, το primary bitmap (σε δεκαεξαδική απεικόνιση) και στην συνέχεια τα πεδία του.
Επειδή όμως τα 64 πεδία δεν ήταν αρκετά χρησιμοποιήθηκε και ένα 2ο bitmap (secondary). Το 1o bit του primary bitmap σηματοδοτούσε την ύπαρξη ή όχι του 2ου bitmap και κατά συνέπεια την ύπαρξη 64 ή 128 πεδίων στο μήνυμα.
Κώδικας:
function TISO8583.FieldExists(fieldNo: integer): boolean;
begin
if fieldNo>64 then
result:=TestBit(PrimaryBitmap,1) and TestBit(SecondaryBitmap,fieldNo-64)
else
result:=TestBit(PrimaryBitmap,fieldNo)
end;
Κάθε πεδίο, απεικονίζεται σε ascii αλλά έχει τύπο και μήκος. Πως ελέγχουμε 128 πεδία ?. Ένα μεγάλο case statement και τελειώσαμε. Ένας άλλος τρόπος είναι να περιγράψουμε τις ιδιότητες των πεδίων και να ελέγχουμε καθοδηγούμενοι από αυτές.
Κώδικας:
type TNibble = 0..15;
T8583FieldTypes = (ftb,ftn,ftx_n,ftns,fta,ftan,ftans,ftz,fta_n);
T8583FieldFormat = (ffNone,ffLLVAR,ffLLLVAR,ffMMDDhhmmss,
ffhhmmss,ffMMDD,ffYYMM,ffYYMMDD,ffx,ffb);
T8583FieldDef = packed record
bit:byte;
kind:T8583FieldTypes;
len:word;
opt:T8583FieldFormat;
end;
T8583FieldDef = packed record
bit:byte;
kind:T8583FieldTypes;
len:word;
opt:T8583FieldFormat;
end;
const FieldDefs : array[1..128] of T8583FieldDef =
((bit: 1; kind: ftb; len: 1; Opt:ffNone),
(bit: 2; kind: ftn; len: 19; Opt:ffLLVAR),
(bit: 3; kind: ftn; len: 6; Opt:ffNone),
..
(bit: 7; kind: ftn; len: 10; Opt:ffMMDDhhmmss),
..
(bit: 12; kind: ftn; len: 6; Opt:ffhhmmss),
(bit: 13; kind: ftn; len: 4; Opt:ffMMDD),
(bit: 14; kind: ftn; len: 4; Opt:ffYYMM),
(bit: 15; kind: ftn; len: 4; Opt:ffMMDD),
..
(bit: 28; kind:ftx_n; len: 8; Opt:ffx),
..
Διαφορετικοί τύποι πεδίων με σταθερό ή μεταβλητό μήκος, στη αποστολή αλλά και στην λήψη. Πεδία μέσα σε πεδία.
Κώδικας:
procedure TISO8583.SetField(fieldNo: integer;const Value: string);
var ok:boolean;
begin
with FieldDefs[fieldNo] do
begin
case opt of
ffLLVAR,
ffLLLVAR : ok:=length(value)<=len;
ffx : ok:=length(Value)=len+1;
ffb : ok:=length(value)=(len div 8)*2
else
ok:=length(Value)=len;
end;
if not ok then
raise Exception.CreateFmt('Invalid Field %d length %d vs %d',
[fieldNo,length(value),len]);
case kind of
ftb : ok:=isHex(Value);
ftn : begin
ok:=isN(Value);
if ok then
case opt of
ffMMDDhhmmss : ok:=isMMDD(copy(value,1,4)) and
isHHMMSS(copy(value,5,6));
ffhhmmss : ok:=isHHMMSS(value);
ffMMDD : ok:=isMMDD(value);
ffYYMM : ok:=isYYMM(value);
ffYYMMDD : ok:=isYYMMDD(value);
end
end;
ftx_n : ok:=(value[1] in ['C','D']) and
isN(copy(Value,2,length(value)-1));
ftns :;
fta :ok:=isA(Value);
ftan :ok:=isAN(Value);
ftans :ok:=isANS(Value);
ftz :;
fta_n :ok:=isN(Value) or isA(value); // a or n
end;
if not ok then
raise Exception.CreateFmt('Invalid Field data %d [%s]',
[FieldNo,Value]);
end;
// Valid field value, store it
fFields[pred(fieldNo)]:=value;
if (fieldNo>64) then
begin
SetBit(SecondaryBitmap,fieldNo-64);
SetBit(PrimaryBitmap,1);
end
else
SetBit(PrimaryBitmap,fieldNo)
end;
TISO8583 = class
private
fFields:TStringList;
fID:string;
fMTI: string;
PrimaryBitmap : array[1..8] of byte; // 16 hex bytes
SecondaryBitmap : array[1..8] of byte;
function GetData: string;
procedure SetData(Value: string);
function GetField(fieldNo: integer): string;
procedure SetField(fieldNo: integer; const Value: string);
function AsString(fieldNo:integer;value:int64):string;
procedure SetBit(var buf;bitNo: integer);
function TestBit(var buf;bitNo: integer): boolean;
protected
function isN(value:string):boolean;
function isANS(value:string):boolean;
function isA(value:string):boolean;
function isAN(value: string): boolean;
..
constructor Create(const aMerchantId,aTerminalId,
aAcquirerId : string); overload; virtual;
public
constructor Create; reintroduce; overload;
constructor Create(const msg:string); overload;
destructor Destroy; override;
procedure AssignTo(source:TISO8583);
function FieldExists(fieldNo:integer):boolean;
procedure Clear; virtual;
property MTI: string read fMTI write fMTI;
property Data:string read GetData write SetData;
property Fields[fieldNo:integer]:string read GetField
write SetField;
end;
Θέλουμε να σπάσουμε ένα ascii iso8583 μήνυμα στα πεδία που το σχηματίζουν.
Κώδικας:
iso8583 := TISO8583.Create;
iso8583.Data:=msg;
procedure TISO8583.SetData(Value: string);
var i,ilen:integer;
s:string;
p:pByte;
w:pword;
b:byte;
begin
Clear;
fID:=copy(value,1,6); delete(value,1,6);
fMTI:=copy(value,1,4); delete(value,1,4);
BitCopy(value, PrimaryBitmap); delete(value,1,16);
if TestBit(PrimaryBitmap,1) then begin
BitCopy(value, SecondaryBitmap); delete(value,1,16);
end;
for i:=1 to 128 do // skip Primary and Secondary bitmaps
if (i<>1) and (i<>65) and fieldExists(i) then
with FieldDefs[i] do begin
case Opt of
ffLLVAR : begin
ilen:=StrToInt(copy(value,1,2));
delete(value,1,2);
end;
ffLLLVAR: begin
ilen:=StrToInt(copy(value,1,3));
delete(value,1,3);
end;
ffx : ilen:=len+1;
ffb : ilen:=(len div 8)*2;
else
{ ffMMDDhhmmss, ffhhmmss, ffMMDD, ffYYMM, ffYYMMDD, ffNone :}
ilen:=len;
end;
s:=copy(value,1,ilen); delete(value,1,ilen);
// αφού πήραμε το περιεχόμενο του πεδίου
// τώρα ας το αποθηκεύσουμε
// property Fields[FieldNo:integer]:string ... write SetField;
Fields[i]:=s;
end;
end;
Συνεχίζεται…