Fixes and tests for indirect transfers. Extended balance.
TrsrDB: fixed Id -> credId autobalance -> make_transfers TrsrDB::Account: added history accessor TrsrDB::Balance: added fieds earned spent and even_until, renamed credit to available TrsrDB::CurrentArrears: fixed account -> debtor in relation declaration schema.sql: fixed triggers deactivated trigger enforceFixedCredit extended Balance with some more fields t/schema.{sql,out} trivial fixes t/schema.t added tests: Get balances after transfers partial use of credit, indirect transfers
This commit is contained in:
parent
bc37ee316b
commit
8459e9ae48
@ -23,7 +23,7 @@ sub import {
|
||||
);
|
||||
}
|
||||
|
||||
sub autobalance {
|
||||
sub make_transfers {
|
||||
my ($self, @pairs) = @_;
|
||||
|
||||
my $from_to = [
|
||||
@ -32,7 +32,7 @@ sub autobalance {
|
||||
];
|
||||
my $to_from = [
|
||||
undef, CurrentArrears => 'billId',
|
||||
payable_with => 'Id', undef
|
||||
payable_with => 'credId', undef
|
||||
];
|
||||
|
||||
my $transfers = $self->resultset('Transfer');
|
||||
|
@ -30,5 +30,9 @@ __PACKAGE__->has_many(
|
||||
__PACKAGE__->has_one(
|
||||
balance => 'TrsrDB::Balance', 'ID'
|
||||
);
|
||||
__PACKAGE__->has_many(
|
||||
history => 'TrsrDB::History',
|
||||
{ 'foreign.account' => 'self.ID' }
|
||||
);
|
||||
|
||||
1;
|
||||
|
@ -4,7 +4,7 @@ package TrsrDB::Balance;
|
||||
use base qw/DBIx::Class::Core/;
|
||||
|
||||
__PACKAGE__->table("Balance");
|
||||
__PACKAGE__->add_columns(qw/ID credit promised arrears/);
|
||||
__PACKAGE__->add_columns(qw/ ID available earned promised spent arrears even_until /);
|
||||
__PACKAGE__->set_primary_key("ID");
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
|
@ -8,12 +8,12 @@ __PACKAGE__->add_columns(qw/billId debtor targetCredit date purpose difference/)
|
||||
__PACKAGE__->set_primary_key("billId");
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
account => 'TrsrDB::Account',
|
||||
{ 'foreign.ID' => 'self.account' }
|
||||
debtor => 'TrsrDB::Account',
|
||||
{ 'foreign.ID' => 'self.debtor' }
|
||||
);
|
||||
|
||||
__PACKAGE__->many_to_many(
|
||||
payable_with => account => 'available_credits'
|
||||
payable_with => debtor => 'available_credits'
|
||||
);
|
||||
|
||||
1;
|
||||
|
126
schema.sql
126
schema.sql
@ -84,20 +84,14 @@ BEGIN
|
||||
END
|
||||
WHERE billId=NEW.billId;
|
||||
|
||||
UPDATE Credit
|
||||
SET value = value + (SELECT m FROM _temp)
|
||||
WHERE credId = (
|
||||
SELECT targetCredit
|
||||
FROM Debit
|
||||
WHERE billId=NEW.billId
|
||||
);
|
||||
|
||||
UPDATE Credit
|
||||
SET spent = spent + CASE
|
||||
WHEN (SELECT c FROM _temp) <= 0
|
||||
THEN RAISE(FAIL, "Credit spent")
|
||||
ELSE
|
||||
(SELECT m FROM _temp)
|
||||
ELSE IFNULL(
|
||||
(SELECT m FROM _temp),
|
||||
RAISE(FAIL,"Oops, lost _temp record before increasing spent")
|
||||
)
|
||||
END
|
||||
WHERE credId=NEW.credId;
|
||||
|
||||
@ -106,6 +100,17 @@ BEGIN
|
||||
WHERE billId=NEW.billId AND credId=NEW.credId
|
||||
;
|
||||
|
||||
UPDATE Credit
|
||||
SET value = value + IFNULL(
|
||||
(SELECT m FROM _temp),
|
||||
RAISE(FAIL, "Oops, lost _temp record before increasing value")
|
||||
)
|
||||
WHERE credId = (
|
||||
SELECT targetCredit
|
||||
FROM Debit
|
||||
WHERE billId=NEW.billId
|
||||
);
|
||||
|
||||
DELETE FROM _temp;
|
||||
|
||||
END;
|
||||
@ -245,12 +250,12 @@ BEGIN
|
||||
WHERE (NEW.spent + IFNULL((SELECT m FROM _temp WHERE c IS NULL AND d IS NULL),0) ) <> OLD.spent;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER enforceFixedCredit
|
||||
BEFORE UPDATE OF value ON Credit
|
||||
BEGIN
|
||||
SELECT RAISE(FAIL, "Credit involved in transactions to revoke at first")
|
||||
WHERE EXISTS (SELECT * FROM Transfer WHERE credId=NEW.credId);
|
||||
END;
|
||||
-- CREATE TRIGGER enforceFixedCredit
|
||||
-- BEFORE UPDATE OF value ON Credit
|
||||
-- BEGIN
|
||||
-- SELECT RAISE(FAIL, "Credit involved in transactions to revoke at first")
|
||||
-- WHERE EXISTS (SELECT * FROM Transfer WHERE credId=NEW.credId);
|
||||
-- END;
|
||||
|
||||
CREATE TRIGGER checkIBANatTransfer
|
||||
BEFORE INSERT ON Debit
|
||||
@ -283,11 +288,43 @@ CREATE VIEW AvailableCredits AS
|
||||
WHERE value != spent
|
||||
;
|
||||
|
||||
|
||||
-- Log of internal transfers
|
||||
CREATE VIEW History AS
|
||||
-- internal transfers with account as source
|
||||
SELECT DATE(timestamp) AS date,
|
||||
d.purpose AS purpose,
|
||||
d.debtor AS account,
|
||||
NULL AS credit,
|
||||
t.amount AS debit,
|
||||
c.account AS contra,
|
||||
d.billId AS billId
|
||||
FROM Transfer t
|
||||
LEFT JOIN Debit AS d ON d.billId = t.billId
|
||||
LEFT JOIN Credit AS c ON c.credId = d.targetCredit
|
||||
-- internal transfers with account as target
|
||||
UNION
|
||||
SELECT DATE(timestamp) AS date,
|
||||
d.purpose AS purpose,
|
||||
c.account AS account,
|
||||
t.amount AS credit,
|
||||
NULL AS debit,
|
||||
d.debtor AS contra,
|
||||
d.billId AS billId
|
||||
FROM Transfer t
|
||||
LEFT JOIN Debit AS d ON d.billId = t.billId
|
||||
LEFT JOIN Credit AS c ON c.credId = d.targetCredit
|
||||
ORDER BY date ASC
|
||||
;
|
||||
|
||||
CREATE VIEW Balance AS
|
||||
SELECT Account.ID AS ID,
|
||||
IFNULL(ac.allCredits,0) AS credit,
|
||||
IFNULL(ac.allCredits,0) AS available,
|
||||
IFNULL(hi.credit,0) AS earned,
|
||||
IFNULL(hi.debit,0) AS spent,
|
||||
IFNULL(pr.allPromises, 0) AS promised,
|
||||
IFNULL(ca.allArrears, 0) AS arrears
|
||||
IFNULL(ca.allArrears, 0) AS arrears,
|
||||
even.until AS even_until
|
||||
FROM Account
|
||||
LEFT OUTER JOIN (
|
||||
SELECT debtor, sum(difference) AS allArrears
|
||||
@ -306,6 +343,21 @@ CREATE VIEW Balance AS
|
||||
JOIN Account a ON a.ID = c.account
|
||||
GROUP BY a.ID
|
||||
) AS pr ON Account.ID=pr.ID
|
||||
LEFT OUTER JOIN (
|
||||
SELECT account,
|
||||
sum(credit) AS credit,
|
||||
sum(debit) AS debit
|
||||
FROM History
|
||||
GROUP BY account
|
||||
) AS hi ON Account.ID=hi.account
|
||||
LEFT OUTER JOIN (
|
||||
SELECT d.debtor AS account,
|
||||
max(d.date) AS until
|
||||
FROM Debit d
|
||||
LEFT OUTER JOIN CurrentArrears ca ON d.debtor = ca.debtor
|
||||
GROUP BY d.debtor, ca.debtor
|
||||
HAVING d.date <= IFNULL( min(ca.date), d.date )
|
||||
) AS even ON Account.ID=even.account
|
||||
;
|
||||
|
||||
CREATE VIEW ReconstructedBankStatement AS
|
||||
@ -328,41 +380,3 @@ CREATE VIEW ReconstructedBankStatement AS
|
||||
WHERE targetCredit IS NULL -- exclude internal transfers
|
||||
ORDER BY date ASC
|
||||
;
|
||||
|
||||
-- History view: All incoming, outgoing payments and internal transfers
|
||||
CREATE VIEW History AS
|
||||
SELECT c.date AS date,
|
||||
c.purpose AS purpose,
|
||||
account,
|
||||
c.value AS credit,
|
||||
NULL AS debit,
|
||||
NULL AS contra,
|
||||
NULL AS billId
|
||||
FROM Credit AS c
|
||||
LEFT OUTER JOIN Debit AS d ON c.credId=d.targetCredit
|
||||
GROUP BY c.credId
|
||||
HAVING count(d.billId) == 0 -- exclude internal transfers
|
||||
UNION -- internal transfers with account as source
|
||||
SELECT DATE(timestamp) AS date,
|
||||
d.purpose AS purpose,
|
||||
d.debtor AS account,
|
||||
NULL AS credit,
|
||||
t.amount AS debit,
|
||||
c.account AS contra,
|
||||
d.billId AS billId
|
||||
FROM Transfer t
|
||||
LEFT JOIN Credit AS c ON c.credId = t.credId
|
||||
LEFT JOIN Debit AS d ON d.billId = t.billId
|
||||
UNION -- internal transfers with account as target
|
||||
SELECT DATE(timestamp) AS date,
|
||||
d.purpose AS purpose,
|
||||
c.account AS account,
|
||||
t.amount AS credit,
|
||||
NULL AS debit,
|
||||
d.debtor AS contra,
|
||||
d.billId AS billId
|
||||
FROM Transfer t
|
||||
LEFT JOIN Debit AS d ON d.billId = t.billId
|
||||
LEFT JOIN Credit AS c ON c.credId = t.credId
|
||||
ORDER BY date ASC
|
||||
;
|
||||
|
@ -1,4 +1,4 @@
|
||||
ID: credit promise debt
|
||||
ID: availbl promise debt
|
||||
--------------------------------
|
||||
Club: 0 +7200 -23450
|
||||
john: 7200 +0 -7200
|
||||
|
12
t/schema.sql
12
t/schema.sql
@ -24,18 +24,18 @@ INSERT INTO Debit VALUES ("MB1605-john", "john", 1, "2016-05-01", "Membership fe
|
||||
("TWX2016/123", "Club", 3, "2016-01-15", "Server Hosting 2016", 23450, 0);
|
||||
|
||||
.separator " "
|
||||
SELECT "ID: credit promise debt";
|
||||
SELECT "ID: availbl promise debt";
|
||||
SELECT "--------------------------------";
|
||||
|
||||
SELECT ID || ":", credit, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("john", "Club");
|
||||
SELECT ID || ":", available, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("john", "Club");
|
||||
|
||||
SELECT "# Reflect john paying its bills all at once ...";
|
||||
INSERT INTO Transfer (billId, credId) VALUES ("MB1605-john", 2), ("MB1606-john", 2), ("MB1607-john", 2), ("MB1608-john", 2), ("MB1609-john", 2), ("MB1610-john", 2), ("MB1611-john", 2), ("MB1612-john", 2), ("MB1701-john", 2), ("MB1702-john", 2), ("MB1703-john", 2), ("MB1704-john", 2);
|
||||
SELECT ID || ":", credit, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("john", "Club");
|
||||
SELECT ID || ":", available, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("john", "Club");
|
||||
|
||||
SELECT "# Charge Club with server hosting provided by alex ...";
|
||||
INSERT INTO Transfer (billId, credId) VALUES ("TWX2016/123", 1);
|
||||
SELECT ID || ":", credit, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("Club", "alex");
|
||||
SELECT ID || ":", available, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("Club", "alex");
|
||||
|
||||
SELECT "# Some updates and deletes that could, unless denied, destroy consistency ...";
|
||||
UPDATE Debit SET paid = 20000 WHERE billId="TWX2016/123";
|
||||
@ -47,11 +47,11 @@ BEGIN TRANSACTION;
|
||||
DELETE FROM Transfer WHERE billId="TWX2016/123";
|
||||
UPDATE Debit SET value = 20000 WHERE billId="TWX2016/123";
|
||||
DELETE FROM Debit WHERE billId="TWX2016/123"; -- *SHOULD* work
|
||||
SELECT ID || ":", credit, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("Club", "alex");
|
||||
SELECT ID || ":", available, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("Club", "alex");
|
||||
ROLLBACK TRANSACTION;
|
||||
|
||||
SELECT '# But let''s rollback that what-if excurse. This is how it currently is ...';
|
||||
SELECT ID || ":", credit, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("Club", "alex");
|
||||
SELECT ID || ":", available, '+' || promised, arrears * -1 FROM Balance WHERE ID in ("Club", "alex");
|
||||
|
||||
SELECT '###################################################################';
|
||||
SELECT '# Now it is your turn: Study the sql code yielding the output above';
|
||||
|
57
t/schema.t
57
t/schema.t
@ -46,7 +46,7 @@ my %months = (
|
||||
while ( my ($num, $month) = each %months ) {
|
||||
my $yy = substr $month, -2;
|
||||
$db->resultset("Debit")->create({
|
||||
billId => "MB$yy$num-john",
|
||||
billId => "MB$yy$num/john",
|
||||
debtor => "john",
|
||||
targetCredit => 1,
|
||||
date => "16-05-01",
|
||||
@ -70,17 +70,60 @@ is $db->resultset("Debit")->search({ debtor => 'Club' })->single->billId, "TWX20
|
||||
|
||||
is_deeply {
|
||||
map { $_->ID => {$_->get_columns} }
|
||||
$db->resultset("Balance")->all
|
||||
$db->resultset("Balance")->search(
|
||||
{}, { columns => [qw|ID available arrears promised|] }
|
||||
)
|
||||
}, {
|
||||
john => { ID => 'john', credit => 7200, arrears => 7200, promised => 0 },
|
||||
Club => { ID => 'Club', credit => 0, arrears => 23450, promised => 7200 },
|
||||
alex => { ID => 'alex', credit => 0, arrears => 0, promised => 23450 },
|
||||
john => { ID=>'john', available=>7200, arrears=>7200, promised=>0 },
|
||||
Club => { ID=>'Club', available=>0, arrears=>23450, promised=>7200 },
|
||||
alex => { ID=>'alex', available=>0, arrears=>0, promised=>23450 },
|
||||
},
|
||||
"Get balances"
|
||||
"Get balances before transfers"
|
||||
;
|
||||
|
||||
# Transfer 72 Euro (6 Euro per month) from john's to Club account.
|
||||
# Transfer same 72 Euro from Club account to alex hosting the web site.
|
||||
is $db->autobalance( (q{*} => q{*}) x 2 ), 14400, 'Automatically balanced credits and debits';
|
||||
is $db->make_transfers( (q{*} => q{*}) x 2 ), 14400, 'Automatically balanced credits and debits';
|
||||
|
||||
is_deeply {
|
||||
map { $_->ID => {$_->get_columns} }
|
||||
$db->resultset("Balance")->search(
|
||||
{}, { columns => [qw|ID available arrears promised|] }
|
||||
)
|
||||
}, {
|
||||
john => { ID=>'john', available=>0, arrears=>0, promised=>0 },
|
||||
Club => { ID=>'Club', available=>0, arrears=>16250, promised=>0 },
|
||||
alex => { ID=>'alex', available=>7200, arrears=>0, promised=>16250 },
|
||||
},
|
||||
"Get balances after transfers"
|
||||
;
|
||||
|
||||
$db->resultset("Account")->create({ ID => "rose", altId => 45, type => 'member' });
|
||||
%months = (
|
||||
'07' => 'July 2016', '08' => 'August 2016',
|
||||
'09' => 'September 2016', '10' => 'October 2016', '11' => 'November 2016', '12' => 'December 2016',
|
||||
);
|
||||
while ( my ($num, $month) = each %months ) {
|
||||
my $yy = substr $month, -2;
|
||||
$db->resultset("Debit")->create({
|
||||
billId => "MB$yy$num/rose",
|
||||
debtor => "rose",
|
||||
targetCredit => 1,
|
||||
date => "16-07-10",
|
||||
purpose => "Membership fee $month",
|
||||
value => 600
|
||||
});
|
||||
}
|
||||
|
||||
my $rose = $db->resultset("Account")->find("rose");
|
||||
$rose->add_to_credits({
|
||||
value => 7200, purpose => "Membership fees until 6/17",
|
||||
date => '16-08-12'
|
||||
});
|
||||
|
||||
$db->make_transfers( q{*} => q{*} );
|
||||
|
||||
is $rose->available_credits->get_column("difference")->sum, 3600, "partial use of credit";
|
||||
is $db->resultset("Balance")->find("alex")->earned, '10800', 'indirect transfers';
|
||||
|
||||
done_testing();
|
||||
|
Loading…
Reference in New Issue
Block a user