From bd9f67040d57869ab76b87bdf5fdacced9ef04e5 Mon Sep 17 00:00:00 2001 From: Rene Krenn Date: Thu, 23 Jul 2015 16:16:03 +0200 Subject: [PATCH] MT#13903 topupvoucher and topupcash +applying profile package and billing mappings +testcase in api-balanceintervals.t +caveats: to meet melita's user story, an additional 'topup_interval' interval start mode will be required. the currently implemented 'topup' start mode restarts intervals upon every topup and therefore does not provide constantinterval lengths. Change-Id: I0a4898783c023749994e94e6909833a42debe259 --- lib/NGCP/Panel/Controller/API/TopupCash.pm | 32 +- .../Panel/Controller/API/TopupVouchers.pm | 92 ++-- lib/NGCP/Panel/Controller/API/Vouchers.pm | 11 + lib/NGCP/Panel/Controller/Voucher.pm | 14 + .../Panel/Form/Balance/BalanceIntervalAPI.pm | 17 +- .../Panel/Form/BillingProfile/Reseller.pm | 6 +- .../Panel/Form/ProfilePackage/PackageAPI.pm | 8 +- .../Panel/Form/ProfilePackage/Reseller.pm | 24 +- lib/NGCP/Panel/Form/Topup/VoucherAPI.pm | 2 +- lib/NGCP/Panel/Form/Voucher/Admin.pm | 2 +- lib/NGCP/Panel/Form/Voucher/Reseller.pm | 10 +- lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm | 9 + lib/NGCP/Panel/Role/API/BalanceIntervals.pm | 4 +- lib/NGCP/Panel/Role/API/ProfilePackages.pm | 10 + lib/NGCP/Panel/Utils/DateTime.pm | 15 + lib/NGCP/Panel/Utils/ProfilePackages.pm | 145 +++++- ngcp_panel.conf | 1 + t/api-balanceintervals.t | 484 +++++++++++++++++- 18 files changed, 774 insertions(+), 112 deletions(-) diff --git a/lib/NGCP/Panel/Controller/API/TopupCash.pm b/lib/NGCP/Panel/Controller/API/TopupCash.pm index 873417b3a0..a76047bd6c 100644 --- a/lib/NGCP/Panel/Controller/API/TopupCash.pm +++ b/lib/NGCP/Panel/Controller/API/TopupCash.pm @@ -8,6 +8,7 @@ use HTTP::Headers qw(); use HTTP::Status qw(:constants); use MooseX::ClassAttribute qw(class_has); use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::ProfilePackages; use Path::Tiny qw(path); use Safe::Isa qw($_isa); BEGIN { extends 'Catalyst::Controller::ActionRole'; } @@ -80,6 +81,7 @@ sub POST :Allow { return; } + $c->model('DB')->set_transaction_isolation('READ COMMITTED'); my $guard = $c->model('DB')->txn_scope_guard; { my $resource = $self->get_valid_post_data( @@ -97,17 +99,41 @@ sub POST :Allow { # the validation, so exclude them here exceptions => [qw/package_id subscriber_id/], ); + my $reseller_id; if($c->user->roles eq "admin") { } elsif($c->user->roles eq "reseller") { - $resource->{reseller_id} = $c->user->reseller_id; + $reseller_id = $c->user->reseller_id; } # subscriber_id, package_id, amount - + my $now = NGCP::Panel::Utils::DateTime::current_local; + my $subscriber = $c->model('DB')->resultset('voip_subscribers')->find($resource->{subscriber_id}); + unless($subscriber) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Unknown subscriber_id.'); + last; + } + my $customer = $subscriber->contract; + unless($customer->status eq 'active') { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Customer contract is not active.'); + last; + } + unless($customer->contact->reseller) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Contract is not a customer contract.'); + last; + } # if reseller, check if subscriber_id belongs to the calling reseller + if($reseller_id && $reseller_id != $customer->contact->reseller_id) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Subscriber customer contract belongs to another reseller.'); + last; + } try { - # update contract balance, update customer package_id, billing profile mappings etc. + my $balance = NGCP::Panel::Utils::ProfilePackages::topup_contract_balance(c => $c, + contract => $customer, + #old_package => $customer->profile_package, + amount => $resource->{amount}, + now => $now, + ); } catch($e) { $c->log->error("failed to create cash topup: $e"); # TODO: user, message, trace, ... $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create cash topup."); diff --git a/lib/NGCP/Panel/Controller/API/TopupVouchers.pm b/lib/NGCP/Panel/Controller/API/TopupVouchers.pm index b3fdbd922b..914ac57170 100644 --- a/lib/NGCP/Panel/Controller/API/TopupVouchers.pm +++ b/lib/NGCP/Panel/Controller/API/TopupVouchers.pm @@ -7,7 +7,9 @@ use Data::HAL::Link qw(); use HTTP::Headers qw(); use HTTP::Status qw(:constants); use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::Voucher; use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::ProfilePackages; use Path::Tiny qw(path); use Safe::Isa qw($_isa); BEGIN { extends 'Catalyst::Controller::ActionRole'; } @@ -16,9 +18,6 @@ require Catalyst::ActionRole::CheckTrailingSlash; require Catalyst::ActionRole::HTTPMethods; require Catalyst::ActionRole::RequireSSL; -use NGCP::Panel::Utils::Voucher; -use NGCP::Panel::Utils::DateTime; - use NGCP::Panel::Form::Topup::VoucherAPI; with 'NGCP::Panel::Role::API'; @@ -83,6 +82,7 @@ sub POST :Allow { return; } + $c->model('DB')->set_transaction_isolation('READ COMMITTED'); my $guard = $c->model('DB')->txn_scope_guard; { my $resource = $self->get_valid_post_data( @@ -98,48 +98,58 @@ sub POST :Allow { form => $form, exceptions => [qw/subscriber_id/], ); - if($c->user->roles eq "admin") { - } elsif($c->user->roles eq "reseller") { - $resource->{reseller_id} = $c->user->reseller_id; - } + #my $reseller_id; + #if($c->user->roles eq "admin") { + #} elsif($c->user->roles eq "reseller") { + # $reseller_id = $c->user->reseller_id; + #} my $code = NGCP::Panel::Utils::Voucher::encrypt_code($c, $resource->{code}); + my $now = NGCP::Panel::Utils::DateTime::current_local; + my $subscriber = $c->model('DB')->resultset('voip_subscribers')->find($resource->{subscriber_id}); + unless($subscriber) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Unknown subscriber_id.'); + last; + } + my $customer = $subscriber->contract; + unless($customer->status eq 'active') { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Customer contract is not active.'); + last; + } + unless($customer->contact->reseller) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Contract is not a customer contract.'); + last; + } + my $voucher = $c->model('DB')->resultset('vouchers')->find({ + code => $code, + used_by_subscriber_id => undef, + valid_until => { '<=' => $now }, + reseller_id => $customer->contact->reseller_id, + },{ + for => 'update', + }); + unless($voucher) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Invalid voucher code or already used.'); + last; + } + if($voucher->customer_id && $customer->id != $voucher->customer_id) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Voucher is reserved for a different customer.'); + last; + } + unless($voucher->reseller_id == $customer->contact->reseller_id) { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, 'Voucher belongs to another reseller.'); + last; + } + # TODO: add and check billing.vouchers.active flag for internal/emergency use + try { - # TODO: add billing.vouchers.active flag for internal/emergency use - - my $now = NGCP::Panel::Utils::DateTime::current_local; - my $subscriber = $c->model('DB')->resultset('voip_subscribers')->find($resource->{subscriber_id}); - unless($subscriber) { - # TODO: error - } - my $customer = $subscriber->contract; - - my $voucher = $c->model('DB')->resultset('voip_subscribers')->find({ - code => $code, - used_by_subscriber_id => undef, - valid_until => { '<=' => $now }, - reseller_id => $customer->contact->reseller_id, # TODO: make unique key code,reseller_id - },{ - for => 'update', - }); - unless($voucher) { - # TODO: invalid code or already used - } - - if($voucher->customer_id && $customer->id != $voucher->customer_id) { - # TODO: error, voucher only to be used by a different customer - } - - if($voucher->reseller_id != $customer->contact->reseller_id) { - # TODO: error, voucher only to be used by a different customer - } - - # TODO: update customer package_id, billing profile mappings etc. - - my $balance = undef; # TODO: get current contract balance - $balance->update({ cash_balance => $balance->cash_balance + $voucher->amount }); - + my $balance = NGCP::Panel::Utils::ProfilePackages::topup_contract_balance(c => $c, + contract => $customer, + #old_package => $customer->profile_package, + voucher => $voucher, + now => $now, + ); $voucher->update({ used_by_subscriber_id => $subscriber->id, diff --git a/lib/NGCP/Panel/Controller/API/Vouchers.pm b/lib/NGCP/Panel/Controller/API/Vouchers.pm index 0b3ebb713c..20c027fb40 100644 --- a/lib/NGCP/Panel/Controller/API/Vouchers.pm +++ b/lib/NGCP/Panel/Controller/API/Vouchers.pm @@ -38,6 +38,17 @@ class_has 'query_params' => ( second => sub {}, }, }, + { + param => 'package_id', + description => 'Filter for vouchers belonging to a specific profile package', + query => { + first => sub { + my $q = shift; + { package_id => $q }; + }, + second => sub {}, + }, + }, ]}, ); diff --git a/lib/NGCP/Panel/Controller/Voucher.pm b/lib/NGCP/Panel/Controller/Voucher.pm index e588806513..4268482873 100644 --- a/lib/NGCP/Panel/Controller/Voucher.pm +++ b/lib/NGCP/Panel/Controller/Voucher.pm @@ -42,6 +42,7 @@ sub voucher_list :Chained('/') :PathPart('voucher') :CaptureArgs(0) { $c->user->billing_data ? { name => "code", "search" => 1, "title" => $c->loc("Code") } : (), { name => "amount", "search" => 1, "title" => $c->loc("Amount") }, { name => "reseller.name", "search" => 1, "title" => $c->loc("Reseller") }, + { name => "profile_package.name", "search" => 1, "title" => $c->loc("Profile Package") }, { name => "valid_until", "search" => 1, "title" => $c->loc("Valid Until") }, { name => "used_at", "search" => 1, "title" => $c->loc("Used At") }, { name => "used_by_subscriber.id", "search" => 1, "title" => $c->loc("Used By Subscriber #") }, @@ -139,6 +140,8 @@ sub edit :Chained('base') :PathPart('edit') { my $params = $c->stash->{voucher}; $params->{valid_until} =~ s/^(\d{4}\-\d{2}\-\d{2}).*$/$1/; $params->{reseller}{id} = delete $params->{reseller_id}; + $params->{customer}{id} = delete $params->{customer_id}; + $params->{package}{id} = delete $params->{package_id}; if($c->user->billing_data) { $params->{code} = NGCP::Panel::Utils::Voucher::decrypt_code($c, $params->{code}); } else { @@ -161,6 +164,7 @@ sub edit :Chained('base') :PathPart('edit') { fields => { 'reseller.create' => $c->uri_for('/reseller/create'), 'customer.create' => $c->uri_for('/customer/create'), + 'package.create' => $c->uri_for('/package/create'), }, back_uri => $c->req->uri, ); @@ -173,7 +177,9 @@ sub edit :Chained('base') :PathPart('edit') { } delete $form->values->{reseller}; $form->values->{customer_id} = $form->values->{customer}{id}; + $form->values->{package_id} = $form->values->{package}{id}; delete $form->values->{customer}; + delete $form->values->{package}; if($form->values->{valid_until} =~ /^\d{4}\-\d{2}\-\d{2}$/) { $form->values->{valid_until} = NGCP::Panel::Utils::DateTime::from_string($form->values->{valid_until}) ->add(days => 1)->subtract(seconds => 1); @@ -190,6 +196,8 @@ sub edit :Chained('base') :PathPart('edit') { }); delete $c->session->{created_objects}->{reseller}; + delete $c->session->{created_objects}->{customer}; + delete $c->session->{created_objects}->{profile_package}; NGCP::Panel::Utils::Message->info( c => $c, desc => $c->loc('Billing voucher successfully updated'), @@ -235,6 +243,7 @@ sub create :Chained('voucher_list') :PathPart('create') :Args(0) { fields => { 'reseller.create' => $c->uri_for('/reseller/create'), 'customer.create' => $c->uri_for('/customer/create'), + 'package.create' => $c->uri_for('/package/create'), }, back_uri => $c->req->uri, ); @@ -248,6 +257,8 @@ sub create :Chained('voucher_list') :PathPart('create') :Args(0) { delete $form->values->{reseller}; $form->values->{customer_id} = $form->values->{customer}{id}; delete $form->values->{customer}; + $form->values->{package_id} = $form->values->{package}{id}; + delete $form->values->{package}; $form->values->{created_at} = NGCP::Panel::Utils::DateTime::current_local; if($form->values->{valid_until} =~ /^\d{4}\-\d{2}\-\d{2}$/) { $form->values->{valid_until} = NGCP::Panel::Utils::DateTime::from_string($form->values->{valid_until}) @@ -257,6 +268,8 @@ sub create :Chained('voucher_list') :PathPart('create') :Args(0) { my $voucher = $c->model('DB')->resultset('vouchers')->create($form->values); $c->session->{created_objects}->{voucher} = { id => $voucher->id }; delete $c->session->{created_objects}->{reseller}; + delete $c->session->{created_objects}->{customer}; + delete $c->session->{created_objects}->{profile_package}; NGCP::Panel::Utils::Message->info( c => $c, desc => $c->loc('Billing voucher successfully created'), @@ -330,6 +343,7 @@ sub voucher_upload :Chained('voucher_list') :PathPart('upload') :Args(0) { ->add(days => 1)->subtract(seconds => 1); } $row->{customer_id} = undef if(defined $row->{customer_id} && $row->{customer_id} eq ""); + $row->{package_id} = undef if(defined $row->{package_id} && $row->{package_id} eq ""); $row->{code} = NGCP::Panel::Utils::Voucher::encrypt_code($c, $row->{code}); push @vouchers, $row; } diff --git a/lib/NGCP/Panel/Form/Balance/BalanceIntervalAPI.pm b/lib/NGCP/Panel/Form/Balance/BalanceIntervalAPI.pm index 74c3007d79..bef9bf357f 100644 --- a/lib/NGCP/Panel/Form/Balance/BalanceIntervalAPI.pm +++ b/lib/NGCP/Panel/Form/Balance/BalanceIntervalAPI.pm @@ -35,14 +35,15 @@ has_field 'billing_profile_id' => ( }, ); -has_field 'invoice_id' => ( - type => 'PosInteger', - #required => 1, - element_attr => { - rel => ['tooltip'], - title => ['The id of the invoice containing this invoice.'] - }, -); +#we leave this out for now +#has_field 'invoice_id' => ( +# type => 'PosInteger', +# #required => 1, +# element_attr => { +# rel => ['tooltip'], +# title => ['The id of the invoice containing this invoice.'] +# }, +#); has_field 'cash_balance' => ( type => 'Money', diff --git a/lib/NGCP/Panel/Form/BillingProfile/Reseller.pm b/lib/NGCP/Panel/Form/BillingProfile/Reseller.pm index 6996752fdd..217c847bb1 100644 --- a/lib/NGCP/Panel/Form/BillingProfile/Reseller.pm +++ b/lib/NGCP/Panel/Form/BillingProfile/Reseller.pm @@ -47,7 +47,8 @@ has_field 'interval_charge' => ( type => 'Money', element_attr => { rel => ['tooltip'], - title => ['The base fee charged per billing interval (a monthly fixed fee, e.g. 10) in Euro/Dollars/etc. This fee can be used on the invoice.'] + #title => ['The base fee charged per billing interval (a monthly fixed fee, e.g. 10) in Euro/Dollars/etc. This fee can be used on the invoice.'] #cents??? + title => ['The base fee charged per billing interval (a monthly fixed fee, e.g. 100) in cents. This fee can be used on the invoice.'] #cents??? }, default => '0', ); @@ -65,7 +66,8 @@ has_field 'interval_free_cash' => ( type => 'Money', element_attr => { rel => ['tooltip'], - title => ['The included free money per billing interval (in Euro, Dollars etc., e.g. 10).'] + #title => ['The included free money per billing interval (in Euro, Dollars etc., e.g. 10).'] #cents??? + title => ['The included free money per billing interval (in cents, e.g. 10000).'] }, default => '0', ); diff --git a/lib/NGCP/Panel/Form/ProfilePackage/PackageAPI.pm b/lib/NGCP/Panel/Form/ProfilePackage/PackageAPI.pm index f2eb083c00..49cead4d3c 100644 --- a/lib/NGCP/Panel/Form/ProfilePackage/PackageAPI.pm +++ b/lib/NGCP/Panel/Form/ProfilePackage/PackageAPI.pm @@ -54,7 +54,7 @@ has_field 'initial_balance' => ( type => 'Money', element_attr => { rel => ['tooltip'], - title => ['The initial balance (in the effective profile\'s currency) that will be set for the very first balance interval.'] + title => ['The initial balance (in cents) that will be set for the very first balance interval.'] }, ); @@ -160,7 +160,7 @@ has_field 'underrun_lock_threshold' => ( type => 'Money', element_attr => { rel => ['tooltip'], - title => ['The balance threshold for the underrun lock level to come into effect.'] + title => ['The balance threshold (in cents) for the underrun lock level to come into effect.'] }, ); @@ -176,7 +176,7 @@ has_field 'underrun_profile_threshold' => ( type => 'Money', element_attr => { rel => ['tooltip'], - title => ['The balance threshold for underrun profiles to come into effect.'] + title => ['The balance threshold (in cents) for underrun profiles to come into effect.'] }, ); @@ -214,7 +214,7 @@ has_field 'service_charge' => ( type => 'Money', element_attr => { rel => ['tooltip'], - title => ['The service charge amount will be subtracted from the voucher amount.'] + title => ['The service charge amount (in cents) will be subtracted from the voucher amount.'] }, ); diff --git a/lib/NGCP/Panel/Form/ProfilePackage/Reseller.pm b/lib/NGCP/Panel/Form/ProfilePackage/Reseller.pm index ad6782b589..28d425f9f3 100644 --- a/lib/NGCP/Panel/Form/ProfilePackage/Reseller.pm +++ b/lib/NGCP/Panel/Form/ProfilePackage/Reseller.pm @@ -39,10 +39,12 @@ has_field 'description' => ( has_field 'initial_balance' => ( type => 'Money', - label => 'Initial Balance', + label => 'Initial Balance', + #inflate_method => sub { return $_[1] * 100.0 }, + #deflate_method => sub { return $_[1] / 100.0 }, element_attr => { rel => ['tooltip'], - title => ['The initial balance (in the effective profile\'s currency) that will be set for the very first balance interval.'] + title => ['The initial balance (in cents) that will be set for the very first balance interval.'] }, default => 0, ); @@ -148,10 +150,12 @@ has_field 'notopup_discard_intervals' => ( has_field 'underrun_lock_threshold' => ( type => 'Money', - label => 'Underrun lock threshold', + label => 'Underrun lock threshold', + #inflate_method => sub { return $_[1] * 100.0 }, + #deflate_method => sub { return $_[1] / 100.0 }, element_attr => { rel => ['tooltip'], - title => ['The balance threshold for the underrun lock level to come into effect.'] + title => ['The balance threshold (in cents) for the underrun lock level to come into effect.'] }, ); @@ -166,10 +170,12 @@ has_field 'underrun_lock_level' => ( has_field 'underrun_profile_threshold' => ( type => 'Money', - label => 'Underrun profile threshold', + label => 'Underrun profile threshold', + #inflate_method => sub { return $_[1] * 100.0 }, + #deflate_method => sub { return $_[1] / 100.0 }, element_attr => { rel => ['tooltip'], - title => ['The balance threshold for underrun profiles to come into effect.'] + title => ['The balance threshold (in cents) for underrun profiles to come into effect.'] }, ); @@ -223,10 +229,12 @@ has_field 'topup_lock_level' => ( has_field 'service_charge' => ( type => 'Money', - label => 'Service Charge', + label => 'Service Charge', + #inflate_method => sub { return $_[1] * 100.0 }, + #deflate_method => sub { return $_[1] / 100.0 }, element_attr => { rel => ['tooltip'], - title => ['The service charge amount will be subtracted from the voucher amount upon every top-up.'] + title => ['The service charge amount (in cents) will be subtracted from the voucher amount upon every top-up.'] }, default => 0, ); diff --git a/lib/NGCP/Panel/Form/Topup/VoucherAPI.pm b/lib/NGCP/Panel/Form/Topup/VoucherAPI.pm index fa40f62be4..63b39e45e3 100644 --- a/lib/NGCP/Panel/Form/Topup/VoucherAPI.pm +++ b/lib/NGCP/Panel/Form/Topup/VoucherAPI.pm @@ -24,7 +24,7 @@ has_field 'subscriber_id' => ( }, ); -has_field 'amount' => ( +has_field 'code' => ( type => 'Text', required => 1, maxlength => 128, diff --git a/lib/NGCP/Panel/Form/Voucher/Admin.pm b/lib/NGCP/Panel/Form/Voucher/Admin.pm index 648661966d..0cb20c2853 100644 --- a/lib/NGCP/Panel/Form/Voucher/Admin.pm +++ b/lib/NGCP/Panel/Form/Voucher/Admin.pm @@ -15,7 +15,7 @@ has_field 'reseller' => ( has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/reseller code amount valid_until customer/], + render_list => [qw/reseller code amount valid_until customer package/], ); diff --git a/lib/NGCP/Panel/Form/Voucher/Reseller.pm b/lib/NGCP/Panel/Form/Voucher/Reseller.pm index ec2bb2104e..7d8707a417 100644 --- a/lib/NGCP/Panel/Form/Voucher/Reseller.pm +++ b/lib/NGCP/Panel/Form/Voucher/Reseller.pm @@ -52,6 +52,14 @@ has_field 'customer' => ( }, ); +has_field 'package' => ( + type => '+NGCP::Panel::Field::ProfilePackage', + #validate_when_empty => 1, + element_attr => { + rel => ['tooltip'], + title => ['The profile package the customer will switch with the top-up.'] + }, +); has_field 'save' => ( type => 'Submit', @@ -63,7 +71,7 @@ has_field 'save' => ( has_block 'fields' => ( tag => 'div', class => [qw/modal-body/], - render_list => [qw/code amount valid_until customer/], + render_list => [qw/code amount valid_until customer package/], ); has_block 'actions' => ( diff --git a/lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm b/lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm index 71485ea73f..0bcbb80c9e 100644 --- a/lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm +++ b/lib/NGCP/Panel/Form/Voucher/ResellerAPI.pm @@ -61,6 +61,15 @@ has_field 'package_id' => ( }, ); +has_field 'package' => ( + type => '+NGCP::Panel::Field::ProfilePackage', + #required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The profile package the customer will switch with the top-up.'] + }, +); + sub validate_valid_until { my ($self, $field) = @_; diff --git a/lib/NGCP/Panel/Role/API/BalanceIntervals.pm b/lib/NGCP/Panel/Role/API/BalanceIntervals.pm index 6be8e2db0f..b16c559a73 100644 --- a/lib/NGCP/Panel/Role/API/BalanceIntervals.pm +++ b/lib/NGCP/Panel/Role/API/BalanceIntervals.pm @@ -67,7 +67,7 @@ sub hal_from_balance { my $is_customer = (defined $contract->contact->reseller_id ? 1 : 0); my $bm_start = NGCP::Panel::Utils::ProfilePackages::get_actual_billing_mapping(c => $c, contract => $contract, now => $item->start); my $profile_at_start = $bm_start->billing_mappings->first->billing_profile; - my $invoice = $item->invoice; + #my $invoice = $item->invoice; my %resource = $item->get_inflated_columns; $resource{cash_balance} /= 100.0; @@ -99,7 +99,7 @@ sub hal_from_balance { Data::HAL::Link->new(relation => 'ngcp:customerbalances', href => sprintf("/api/customerbalances/%d", $contract->id)) ) : Data::HAL::Link->new(relation => 'ngcp:contracts', href => sprintf("/api/contracts/%d", $contract->id)) ), Data::HAL::Link->new(relation => 'ngcp:billingprofiles', href => sprintf("/api/billingprofiles/%d", $profile_at_start->id)), - ($invoice ? Data::HAL::Link->new(relation => 'ngcp:invoices', href => sprintf("/api/invoices/%d", $invoice->id)) : ()), + #($invoice ? Data::HAL::Link->new(relation => 'ngcp:invoices', href => sprintf("/api/invoices/%d", $invoice->id)) : ()), ], relation => 'ngcp:'.$self->resource_name, ); diff --git a/lib/NGCP/Panel/Role/API/ProfilePackages.pm b/lib/NGCP/Panel/Role/API/ProfilePackages.pm index 540220e708..2ecabdea7e 100644 --- a/lib/NGCP/Panel/Role/API/ProfilePackages.pm +++ b/lib/NGCP/Panel/Role/API/ProfilePackages.pm @@ -42,6 +42,11 @@ sub hal_from_item { $resource{initial_profiles} = _get_profiles_mappings($item,'initial_profiles'); $resource{topup_profiles} = _get_profiles_mappings($item,'topup_profiles'); $resource{underrun_profiles} = _get_profiles_mappings($item,'underrun_profiles'); + + #$resource{initial_balance} /= 100.0 if exists $resource{initial_balance} && defined $resource{initial_balance}; #prevent auto-vivivication.. + #$resource{service_charge} /= 100.0 if exists $resource{service_charge} && defined $resource{service_charge}; + #$resource{underrun_lock_threshold} /= 100.0 if exists $resource{underrun_lock_threshold} && defined $resource{underrun_lock_threshold}; + #$resource{underrun_profile_threshold} /= 100.0 if exists $resource{underrun_profile_threshold} && defined $resource{underrun_profile_threshold}; my @profile_links = (); my @network_links = (); @@ -120,6 +125,11 @@ sub update_item { delete $resource->{id}; my $schema = $c->model('DB'); + #$resource{initial_balance} *= 100.0 if exists $resource{initial_balance} && defined $resource{initial_balance}; #prevent auto-vivivication.. + #$resource{service_charge} *= 100.0 if exists $resource{service_charge} && defined $resource{service_charge}; + #$resource{underrun_lock_threshold} *= 100.0 if exists $resource{underrun_lock_threshold} && defined $resource{underrun_lock_threshold}; + #$resource{underrun_profile_threshold} *= 100.0 if exists $resource{underrun_profile_threshold} && defined $resource{underrun_profile_threshold}; + $form //= $self->get_form($c); ## TODO: for some reason, formhandler lets missing reseller slip thru $resource->{reseller_id} //= undef; diff --git a/lib/NGCP/Panel/Utils/DateTime.pm b/lib/NGCP/Panel/Utils/DateTime.pm index 35fdf10155..4f1669a799 100644 --- a/lib/NGCP/Panel/Utils/DateTime.pm +++ b/lib/NGCP/Panel/Utils/DateTime.pm @@ -25,6 +25,11 @@ sub infinite_past { #$dt->epoch calls should be okay if perl >= 5.12.0 } +sub is_infinite_past { + my $dt = shift; + return $dt->year <= 1000; +} + sub infinite_future { #... to '9999-12-31 23:59:59' return DateTime->new(year => 9999, month => 12, day => 31, hour => 23, minute => 59, second => 59, @@ -37,6 +42,16 @@ sub infinite_future { ); } +sub is_infinite_future { + my $dt = shift; + return $dt->year >= 9999; +} + +sub is_infinite { + my $dt = shift; + return is_infinite_past($dt) || is_infinite_future($dt); +} + sub set_fake_time { my ($o) = @_; if (defined $o) { diff --git a/lib/NGCP/Panel/Utils/ProfilePackages.pm b/lib/NGCP/Panel/Utils/ProfilePackages.pm index 4e7817f2f6..e9f1c73b98 100644 --- a/lib/NGCP/Panel/Utils/ProfilePackages.pm +++ b/lib/NGCP/Panel/Utils/ProfilePackages.pm @@ -23,6 +23,7 @@ use constant _DEFAULT_INITIAL_BALANCE => 0.0; use constant _TOPUP_START_MODE => 'topup'; use constant _1ST_START_MODE => '1st'; use constant _CREATE_START_MODE => 'create'; +#use constant _TOPUP_INTERVAL_START_MODE => 'topup_interval'; use constant _START_MODE_PRESERVE_EOM => { _TOPUP_START_MODE . '' => 0, _1ST_START_MODE . '' => 0, @@ -55,10 +56,11 @@ sub get_contract_balance { sub resize_actual_contract_balance { my %params = @_; - my($c,$contract,$old_package,$actual_balance,$now,$schema) = @params{qw/c contract old_package balance now schema/}; + my($c,$contract,$old_package,$actual_balance,$is_topup,$now,$schema) = @params{qw/c contract old_package balance is_topup now schema/}; $schema //= $c->model('DB'); $contract = $schema->resultset('contracts')->find({id => $contract->id},{for => 'update'}); #lock record + $is_topup //= 0; return $actual_balance unless defined $contract->contact->reseller_id; @@ -66,18 +68,35 @@ sub resize_actual_contract_balance { my $new_package = $contract->profile_package; my ($old_start_mode,$new_start_mode); + my $create_next_balance = 0; if (defined $old_package && !defined $new_package) { $old_start_mode = $old_package->balance_interval_start_mode if $old_package->balance_interval_start_mode ne _DEFAULT_START_MODE; $new_start_mode = _DEFAULT_START_MODE; } elsif (!defined $old_package && defined $new_package) { $old_start_mode = _DEFAULT_START_MODE; $new_start_mode = $new_package->balance_interval_start_mode if $new_package->balance_interval_start_mode ne _DEFAULT_START_MODE; - } elsif (defined $old_package && defined $new_package && $old_package->balance_interval_start_mode ne $new_package->balance_interval_start_mode) { #&& $old_package->id != $new_package->id ? - $old_start_mode = $old_package->balance_interval_start_mode; - $new_start_mode = $new_package->balance_interval_start_mode; + $create_next_balance = _TOPUP_START_MODE eq $new_package->balance_interval_start_mode && $is_topup; + } elsif (defined $old_package && defined $new_package) { + if ($old_package->balance_interval_start_mode ne $new_package->balance_interval_start_mode) { #&& $old_package->id != $new_package->id ? + $old_start_mode = $old_package->balance_interval_start_mode; + $new_start_mode = $new_package->balance_interval_start_mode; + $create_next_balance = _TOPUP_START_MODE eq $new_package->balance_interval_start_mode && $is_topup; + } elsif (_TOPUP_START_MODE eq $new_package->balance_interval_start_mode && $is_topup) { + $old_start_mode = _TOPUP_START_MODE; + $new_start_mode = _TOPUP_START_MODE; + $create_next_balance = 1; + } + #} elsif (_TOPUP_INTERVAL_START_MODE eq $new_package->balance_interval_start_mode && $is_topup) { + # $old_start_mode = _TOPUP_INTERVAL_START_MODE; + # $new_start_mode = _TOPUP_INTERVAL_START_MODE; + # xx$create_next_balance = 1; + #} + + + } - if ($old_start_mode && $new_start_mode) { + if ($old_start_mode && $new_start_mode && $actual_balance->start < $now) { my $end_of_resized_interval = _get_resized_interval_end(ctime => $now, create_timestamp => $contract->create_timestamp // $contract->modify_timestamp, start_mode => $new_start_mode); @@ -92,6 +111,15 @@ sub resize_actual_contract_balance { end => $end_of_resized_interval, @$resized_balance_values, }); + $actual_balance->discard_changes(); + if ($create_next_balance) { + $actual_balance = catchup_contract_balances(schema => $schema, + contract => $contract, + old_package => $old_package, + now => $end_of_resized_interval->clone->add(seconds => 1), + ); + } + #catchup_contract_balances(schema => $schema, # contract => $contract, # old_package => $new_package, @@ -119,7 +147,7 @@ sub catchup_contract_balances { $schema //= $c->model('DB'); $contract = $schema->resultset('contracts')->find({id => $contract->id},{for => 'update'}); #lock record $now //= $contract->modify_timestamp; - $old_package //= $contract->profile_package; + $old_package = $contract->profile_package if !exists $params{old_package}; my ($start_mode,$interval_unit,$interval_value,$carry_over_mode,$has_package); @@ -135,10 +163,10 @@ sub catchup_contract_balances { $has_package = 0; } + #my $first_balance = $contract->contract_balances->search(undef,{ order_by => { '-asc' => 'start'},})->first; my $last_balance = $contract->contract_balances->search(undef,{ order_by => { '-desc' => 'end'},})->first; my $last_profile; - #my $end_of_today = $now->clone->truncate(to => 'day')->add(days => 1)->subtract(seconds => 1); - while ($last_balance && $last_balance->end < $now) { + while ($last_balance && !NGCP::Panel::Utils::DateTime::is_infinite_future($last_balance->end) && $last_balance->end < $now) { #comparison takes 100++ sec if loaded lastbalance contains +inf my $start_of_next_interval = $last_balance->end->clone->add(seconds => 1); my $bm_actual; @@ -201,10 +229,83 @@ sub catchup_contract_balances { } -sub XXtopup_create_contract_balance { +sub topup_contract_balance { my %params = @_; - my($c,$contract,$now,$profile,$schema) = @params{qw/c contract now profile schema/}; - return create_initial_contract_balance(c => $c,contract => $contract,now => $now,$profile,$schema,is_topup => 1); + my($c,$contract,$package,$voucher,$amount,$now,$schema) = @params{qw/c contract package voucher amount now schema/}; + + $schema //= $c->model('DB'); + $contract = $schema->resultset('contracts')->find({id => $contract->id},{for => 'update'}); #lock record + $now //= NGCP::Panel::Utils::DateTime::current_local; + + my $voucher_package = ($voucher ? $voucher->profile_package : $package); + my $old_package = $contract->profile_package; + $package = $voucher_package // $old_package; + my $topup_amount = ($voucher ? $voucher->amount : $amount) // 0.0; + + my $mappings_to_create = []; + if ($package) { #always apply (old or new) topup profiles + $topup_amount -= $package->service_charge; + + my $bm_actual = get_actual_billing_mapping(c => $c, contract => $contract, now => $now); + my $product_id = $bm_actual->billing_mappings->first->product->id; + foreach my $mapping ($package->topup_profiles->all) { + push(@$mappings_to_create,{ #assume not terminated, + billing_profile_id => $mapping->profile_id, + network_id => $mapping->network_id, + product_id => $product_id, + start_date => $now, + end_date => undef, + }); + } + } + + if ($voucher_package && (!$old_package || $voucher_package->id != $old_package->id)) { + $contract->update({ profile_package_id => $voucher_package->id, + #modify_timestamp => $now, + }); + } + + foreach my $mapping (@$mappings_to_create) { + $contract->billing_mappings->create($mapping); + } + $contract->discard_changes(); + + my $balance = catchup_contract_balances(c => $c, + contract => $contract, + old_package => $old_package, + now => $now); + + my ($is_timely,$timely_duration_unit,$timely_duration_value) = (0,undef,undef); + if ($old_package + && ($timely_duration_unit = $old_package->timely_duration_unit) + && ($timely_duration_value = $old_package->timely_duration_value)) { + my $timely_end; + if (_TOPUP_START_MODE ne $old_package->balance_interval_start_mode) { + $timely_end = $balance->end; + } else { + $timely_end = _add_interval($balance->start,$old_package->balance_interval_unit,$old_package->balance_interval_value, + _START_MODE_PRESERVE_EOM->{$old_package->balance_interval_start_mode} ? $contract->create_timestamp : undef)->subtract(seconds => 1); + } + my $timely_start = _add_interval($timely_end,$timely_duration_unit,-1 * $timely_duration_value)->add(seconds => 1); + $timely_start = $balance->start if $timely_start < $balance->start; + + $is_timely = ($now >= $timely_start && $now <= $timely_end ? 1 : 0); + } + + $balance = resize_actual_contract_balance(c => $c, + contract => $contract, + old_package => $old_package, + balance => $balance, + now => $now, + is_topup => 1, + ); + + $balance->update({ cash_balance => $balance->cash_balance + $topup_amount, + topup_count => $balance->topup_count + 1, + timely_topup_count => $balance->timely_topup_count + $is_timely}); + $balance->discard_changes(); + + return $balance; } sub create_initial_contract_balance { @@ -243,7 +344,7 @@ sub create_initial_contract_balance { start_mode => $start_mode, now => $now, profile => $profile, - initial_balance => $initial_balance * 100.0, + initial_balance => $initial_balance, # * 100.0, ); #my $balance; @@ -305,7 +406,7 @@ sub _get_balance_values { my $ratio; if ($last_balance) { - if (_CARRY_OVER_MODE eq $carry_over_mode || (_CARRY_OVER_TIMELY_MODE eq $carry_over_mode && $last_balance->timely_count > 0)) { + if (_CARRY_OVER_MODE eq $carry_over_mode || (_CARRY_OVER_TIMELY_MODE eq $carry_over_mode && $last_balance->timely_topup_count > 0)) { #if (!defined $last_profile) { # my $bm_last = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $last_balance->start); #end); !? # $last_profile = $bm_last->billing_mappings->first->billing_profile; @@ -379,6 +480,17 @@ sub _get_balance_interval_start_end { } else { $etime = NGCP::Panel::Utils::DateTime::infinite_future; } + #if (_TOPUP_START_MODE eq $start_mode) { + # $etime = NGCP::Panel::Utils::DateTime::infinite_future; + #} elsif (_TOPUP_INTERVAL_START_MODE eq $start_mode) { + # if ($first_balance) { + # $etime = _add_interval($stime,$interval_unit,$interval_value,_START_MODE_PRESERVE_EOM->{$start_mode} ? $first_balance->end : undef)->subtract(seconds => 1); + # } else { + # $etime = NGCP::Panel::Utils::DateTime::infinite_future; + # } + #} else { + # $etime = _add_interval($stime,$interval_unit,$interval_value,_START_MODE_PRESERVE_EOM->{$start_mode} ? $create : undef)->subtract(seconds => 1); + #} } return ($stime,$etime); @@ -409,6 +521,10 @@ sub _get_resized_interval_end { return $ctime->clone; #->add(seconds => 1); #return NGCP::Panel::Utils::DateTime::infinite_future; } + #} elsif (_TOPUP_INTERVAL_START_MODE eq $start_mode) { + # return $ctime->clone; #->add(seconds => 1); + # #return NGCP::Panel::Utils::DateTime::infinite_future; + #} return undef; } @@ -421,6 +537,9 @@ sub _get_interval_start { } elsif (_TOPUP_START_MODE eq $start_mode) { return $ctime->clone; #->truncate(to => 'day'); } + #} elsif (_TOPUP_INTERVAL_START_MODE eq $start_mode) { + # return $ctime->clone; #->truncate(to => 'day'); + #} return undef; } diff --git a/ngcp_panel.conf b/ngcp_panel.conf index 6f97be7f3e..cbb81ab808 100644 --- a/ngcp_panel.conf +++ b/ngcp_panel.conf @@ -53,6 +53,7 @@ log4perl.appender.Default.layout.ConversionPattern=%d{ISO8601} [%p] [%F +%L] %m{ element_order amount element_order valid_until element_order customer_id + element_order package_id diff --git a/t/api-balanceintervals.t b/t/api-balanceintervals.t index 30a8f5cb50..510ca905d6 100644 --- a/t/api-balanceintervals.t +++ b/t/api-balanceintervals.t @@ -69,6 +69,8 @@ if ($is_local_env) { ); } +my $infinite_future; + { my $future = NGCP::Panel::Utils::DateTime::infinite_future; my $past = NGCP::Panel::Utils::DateTime::infinite_past; @@ -77,7 +79,8 @@ if ($is_local_env) { my $dtf = DateTime::Format::Strptime->new( pattern => '%F %T', ); - is($dtf->format_datetime($future),'9999-12-31 23:59:59','check if infinite future is 9999-12-31 23:59:59'); + $infinite_future = $dtf->format_datetime($future); + is($infinite_future,'9999-12-31 23:59:59','check if infinite future is 9999-12-31 23:59:59'); is($dtf->format_datetime($past),'1000-01-01 00:00:00','check if infinite past is 1000-01-01 00:00:00'); foreach my $offset ((0,'+'. 80*365*24*60*60 .'s','-'. 80*365*24*60*60 .'s')) { @@ -118,22 +121,6 @@ if ($is_local_env) { my $t = time; my $default_reseller_id = 1; -$req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/'); -$req->header('Content-Type' => 'application/json'); -$req->header('Prefer' => 'return=representation'); -$req->content(JSON::to_json({ - name => "test profile $t", - handle => "testprofile$t", - reseller_id => $default_reseller_id, -})); -$res = $ua->request($req); -is($res->code, 201, "POST test billing profile"); -my $billingprofile_uri = $uri.'/'.$res->header('Location'); -$req = HTTP::Request->new('GET', $billingprofile_uri); -$res = $ua->request($req); -is($res->code, 200, "fetch POSTed billing profile"); -my $billingprofile = JSON::from_json($res->decoded_content); - # first, create a contact $req = HTTP::Request->new('POST', $uri.'/api/customercontacts/'); $req->header('Content-Type' => 'application/json'); @@ -150,12 +137,95 @@ $res = $ua->request($req); is($res->code, 200, "fetch customer contact"); my $custcontact = JSON::from_json($res->decoded_content); +$req = HTTP::Request->new('POST', $uri.'/api/domains/'); +$req->header('Content-Type' => 'application/json'); +$req->content(JSON::to_json({ + domain => 'test' . ($t-1) . '.example.org', + reseller_id => $default_reseller_id, +})); +$res = $ua->request($req); +is($res->code, 201, "POST test domain"); +$req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); +$res = $ua->request($req); +is($res->code, 200, "fetch POSTed test domain"); +my $domain = JSON::from_json($res->decoded_content); + + my %customer_map :shared = (); +my $package_map = {}; +my $voucher_map = {}; +my $subscriber_map = {}; my $profile_map = {}; +my $billingprofile = _create_billing_profile("test_default"); + if (_get_allow_fake_client_time()) { + { + my $network_a = _create_billing_network_a(); + my $network_b = _create_billing_network_b(); + + my $profile_base_any = _create_billing_profile('BASE_ANY'); + my $profile_base_a = _create_billing_profile('BASE_NETWORK_A'); + my $profile_base_b = _create_billing_profile('BASE_NETWORK_B'); + + my $profile_silver_a = _create_billing_profile('SILVER_NETWORK_A'); + my $profile_silver_b = _create_billing_profile('SILVER_NETWORK_B'); + + my $profile_gold_a = _create_billing_profile('GOLD_NETWORK_A'); + my $profile_gold_b = _create_billing_profile('GOLD_NETWORK_B'); + + my $base_package = _create_base_profile_package($profile_base_any,$profile_base_a,$profile_base_b,$network_a,$network_b); + my $silver_package = _create_silver_profile_package($base_package,$profile_silver_a,$profile_silver_b,$network_a,$network_b); + my $extension_package = _create_extension_profile_package($base_package,$profile_silver_a,$profile_silver_b,$network_a,$network_b); + my $gold_package = _create_gold_profile_package($base_package,$profile_gold_a,$profile_gold_b,$network_a,$network_b); + + _set_time(NGCP::Panel::Utils::DateTime::from_string('2015-06-03 13:00:00')); + + my $v_silver_1 = _create_voucher(10,'SILVER1'.$t,undef,$silver_package); + my $v_extension_1 = _create_voucher(2,'EXTENSION1'.$t,undef,$extension_package); + my $v_gold_1 = _create_voucher(20,'GOLD1'.$t,undef,$gold_package); + + _set_time(NGCP::Panel::Utils::DateTime::from_string('2015-06-03 13:00:00')); + my $customer_x = _create_customer($base_package); + my $subscriber_x = _create_subscriber($customer_x); + + _set_time(NGCP::Panel::Utils::DateTime::from_string('2015-06-21 13:00:00')); + + _perform_topup_voucher($subscriber_x,$v_silver_1); + + _set_time(NGCP::Panel::Utils::DateTime::from_string('2015-07-21 13:00:00')); + + _check_interval_history($customer_x,[ + { start => '~2015-06-03 13:00:00', stop => '~2015-06-21 13:00:00', cash => 0, profile => $profile_base_b->{id} }, + { start => '~2015-06-21 13:00:00', stop => $infinite_future, cash => 8, profile => $profile_silver_b->{id} }, + ]); + + #_set_time(NGCP::Panel::Utils::DateTime::from_string('2015-06-03 13:00:00')); + #my $customer_y = _create_customer($base_package); + #my $subscriber_y = _create_subscriber($customer_y); + + #_set_time(NGCP::Panel::Utils::DateTime::from_string('2015-03-04 13:00:00')); + + #_perform_topup_voucher($subscriber,$v_extension_1); + + # #my $voucher = _create_voucher(10,'A'.$t); + # + # #my $customer = _create_customer(); + # + # #$voucher = _create_voucher(11,'B'.$t,$customer); + # + # my $prof_package_topup20 = _create_profile_package('topup'); + # + # my $voucher = _create_voucher(20,'C'.$t,undef,$prof_package_topup20); + + + _set_time(); + } + + + my $prof_package_create30d = _create_profile_package('create','day',30); my $prof_package_1st30d = _create_profile_package('1st','day',30); @@ -170,11 +240,16 @@ if (_get_allow_fake_client_time()) { { _set_time(NGCP::Panel::Utils::DateTime::from_string('2014-12-30 13:00:00')); - + + my $customer_topup = _create_customer($prof_package_topup); #create closest to now my $customer_wo = _create_customer(); my $customer_create1m = _create_customer($prof_package_create1m); _set_time(NGCP::Panel::Utils::DateTime::from_string('2015-04-02 02:00:00')); + + _check_interval_history($customer_topup,[ + { start => '~2014-12-30 13:00:00', stop => $infinite_future}, + ]); _check_interval_history($customer_wo,[ { start => '2014-12-01 00:00:00', stop => '2014-12-31 23:59:59'}, @@ -189,7 +264,7 @@ if (_get_allow_fake_client_time()) { { start => '2015-01-30 00:00:00', stop => '2015-02-27 23:59:59'}, { start => '2015-02-28 00:00:00', stop => '2015-03-29 23:59:59'}, { start => '2015-03-30 00:00:00', stop => '2015-04-29 23:59:59'}, - ]); + ]); _set_time(); } @@ -206,7 +281,7 @@ if (_get_allow_fake_client_time()) { $ts = '2014-03-01 13:00:00'; _set_time(NGCP::Panel::Utils::DateTime::from_string($ts)); - + _check_interval_history($customer,[ { start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'}, { start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'}, @@ -223,7 +298,7 @@ if (_get_allow_fake_client_time()) { $ts = '2014-04-01 13:00:00'; _set_time(NGCP::Panel::Utils::DateTime::from_string($ts)); - + _check_interval_history($customer,[ { start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'}, { start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'}, @@ -273,7 +348,7 @@ if (_get_allow_fake_client_time()) { ]); _switch_package($customer,$prof_package_1st1m); - + _check_interval_history($customer,[ { start => '2014-01-01 00:00:00', stop => '2014-01-31 23:59:59'}, { start => '2014-02-01 00:00:00', stop => '2014-02-28 23:59:59'}, @@ -281,7 +356,7 @@ if (_get_allow_fake_client_time()) { { start => '2014-03-07 00:00:00', stop => '2014-04-30 23:59:59'}, { start => '2014-05-01 00:00:00', stop => '2014-05-31 23:59:59'}, ]); - + my $t1 = $ts; $ts = '2014-08-03 13:00:00'; _set_time(NGCP::Panel::Utils::DateTime::from_string($ts)); @@ -314,8 +389,55 @@ if (_get_allow_fake_client_time()) { _check_interval_history($customer,[ { start => '2014-08-07 00:00:00', stop => '2014-08-20 23:59:59'}, { start => '2014-08-21 00:00:00', stop => '2014-09-30 23:59:59'}, + ],NGCP::Panel::Utils::DateTime::from_string($t1)); + + $t1 = $ts; + #my $t1 = '2014-09-03 13:00:00'; + $ts = '2014-10-04 13:00:00'; + _set_time(NGCP::Panel::Utils::DateTime::from_string($ts)); + + _switch_package($customer,$prof_package_topup); + + _check_interval_history($customer,[ + { start => '2014-10-01 00:00:00', stop => '~2014-10-04 13:00:00'}, + { start => '~2014-10-04 13:00:00', stop => $infinite_future}, + ],NGCP::Panel::Utils::DateTime::from_string($t1)); + + my $voucher = _create_voucher(10,'topup_start_mode_test'.$t,$customer,$prof_package_create1m); + my $subscriber = _create_subscriber($customer); + + #_check_interval_history($customer,[ + # { start => '2014-10-01 00:00:00', stop => '~2014-10-04 13:00:00'}, + # { start => '~2014-10-04 13:00:00', stop => $infinite_future}, + #],NGCP::Panel::Utils::DateTime::from_string($t1)); + + _perform_topup_voucher($subscriber,$voucher); + + _check_interval_history($customer,[ + { start => '2014-10-01 00:00:00', stop => '~2014-10-04 13:00:00'}, + { start => '~2014-10-04 13:00:00', stop => '2014-10-06 23:59:59'}, ],NGCP::Panel::Utils::DateTime::from_string($t1)); + $t1 = $ts; + $ts = '2014-12-09 13:00:00'; + _set_time(NGCP::Panel::Utils::DateTime::from_string($ts)); + + _check_interval_history($customer,[ + { start => '~2014-10-04 13:00:00', stop => '2014-10-06 23:59:59'}, + { start => '2014-10-07 00:00:00', stop => '2014-11-06 23:59:59'}, + { start => '2014-11-07 00:00:00', stop => '2014-12-06 23:59:59'}, + { start => '2014-12-07 00:00:00', stop => '2015-01-06 23:59:59'}, + ],NGCP::Panel::Utils::DateTime::from_string($t1)); + + _switch_package($customer); + + _check_interval_history($customer,[ + { start => '~2014-10-04 13:00:00', stop => '2014-10-06 23:59:59'}, + { start => '2014-10-07 00:00:00', stop => '2014-11-06 23:59:59'}, + { start => '2014-11-07 00:00:00', stop => '2014-12-06 23:59:59'}, + { start => '2014-12-07 00:00:00', stop => '2014-12-31 23:59:59'}, + ],NGCP::Panel::Utils::DateTime::from_string($t1)); + _set_time(); } @@ -422,7 +544,7 @@ sub _check_interval_history { my $i = 0; my $limit = ''; $limit = '&start=' . DateTime::Format::ISO8601->parse_datetime($limit_dt) if defined $limit_dt; - my $label = 'interval history of contract with ' . ($customer->{profile_package_id} ? 'package ' . $profile_map->{$customer->{profile_package_id}}->{name} : 'no package') . ': '; + my $label = 'interval history of contract with ' . ($customer->{profile_package_id} ? 'package ' . $package_map->{$customer->{profile_package_id}}->{name} : 'no package') . ': '; my $nexturi = $uri.'/api/balanceintervals/'.$customer->{id}.'/?page=1&rows=10&order_by_direction=asc&order_by=start'.$limit; do { $req = HTTP::Request->new('GET',$nexturi); @@ -495,12 +617,39 @@ sub _compare_interval { my ($got,$expected,$label) = @_; if ($expected->{start}) { - is(NGCP::Panel::Utils::DateTime::from_string($got->{start}),NGCP::Panel::Utils::DateTime::from_string($expected->{start}),$label . "check interval " . $got->{id} . " start timestmp"); + #is(NGCP::Panel::Utils::DateTime::from_string($got->{start}),NGCP::Panel::Utils::DateTime::from_string($expected->{start}),$label . "check interval " . $got->{id} . " start timestmp"); + if (substr($expected->{start},0,1) eq '~') { + _is_ts_approx($got->{start},$expected->{start},$label . "check interval " . $got->{id} . " start timestamp"); + } else { + is($got->{start},$expected->{start},$label . "check interval " . $got->{id} . " start timestmp"); + } } if ($expected->{stop}) { - is(NGCP::Panel::Utils::DateTime::from_string($got->{stop}),NGCP::Panel::Utils::DateTime::from_string($expected->{stop}),$label . "check interval " . $got->{id} . " stop timestmp"); + #is(NGCP::Panel::Utils::DateTime::from_string($got->{stop}),NGCP::Panel::Utils::DateTime::from_string($expected->{stop}),$label . "check interval " . $got->{id} . " stop timestmp"); + if (substr($expected->{stop},0,1) eq '~') { + _is_ts_approx($got->{stop},$expected->{stop},$label . "check interval " . $got->{id} . " stop timestamp"); + } else { + is($got->{stop},$expected->{stop},$label . "check interval " . $got->{id} . " stop timestmp"); + } } + if ($expected->{cash}) { + is($got->{cash_balance},$expected->{cash},$label . "check interval " . $got->{id} . " cash balance"); + } + + if ($expected->{profile}) { + is($got->{profile_id},$expected->{profile_id},$label . "check interval " . $got->{id} . " billing profile"); + } + +} + +sub _is_ts_approx { + my ($got,$expected,$label) = @_; + $got = NGCP::Panel::Utils::DateTime::from_string($got); + $expected = NGCP::Panel::Utils::DateTime::from_string(substr($expected,1)); + my $lower = $expected->clone->subtract(seconds => 5); + my $upper = $expected->clone->add(seconds => 5); + ok($got >= $lower && $got <= $upper,$label . ' approximately (' . $got . ')'); } sub _fetch_intervals_worker { @@ -573,7 +722,7 @@ sub _switch_package { [ { op => 'replace', path => '/profile_package_id', value => ($package ? $package->{id} : undef) } ] )); $res = $ua->request($req); - is($res->code, 200, "patch customer from " . ($customer->{profile_package_id} ? 'package ' . $profile_map->{$customer->{profile_package_id}}->{name} : 'no package') . " to " . + is($res->code, 200, "patch customer from " . ($customer->{profile_package_id} ? 'package ' . $package_map->{$customer->{profile_package_id}}->{name} : 'no package') . " to " . ($package ? $package->{name} : 'no package')); return JSON::from_json($res->decoded_content); @@ -625,11 +774,290 @@ sub _create_profile_package { $res = $ua->request($req); is($res->code, 200, "fetch POSTed profilepackage - '" . $name . "'"); my $package = JSON::from_json($res->decoded_content); - $profile_map->{$package->{id}} = $package; + $package_map->{$package->{id}} = $package; return $package; } +sub _create_billing_network_a { + + $req = HTTP::Request->new('POST', $uri.'/api/billingnetworks/'); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json({ + name => "test billing network A ".$t, + description => "test billing network A description ".$t, + reseller_id => $default_reseller_id, + blocks => [{ip=>'fdfe::5a55:caff:fefa:9089',mask=>128}, + {ip=>'fdfe::5a55:caff:fefa:908a'}, + {ip=>'fdfe::5a55:caff:fefa:908b',mask=>128},], + })); + $res = $ua->request($req); + is($res->code, 201, "POST test billingnetwork A"); + $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); + $res = $ua->request($req); + is($res->code, 200, "fetch POSTed billingnetwork A"); + my $billingnetwork = JSON::from_json($res->decoded_content); +} + +sub _create_billing_network_b { + + $req = HTTP::Request->new('POST', $uri.'/api/billingnetworks/'); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json({ + name => "test billing network B ".$t, + description => "FIRST test billing network B description ".$t, + reseller_id => $default_reseller_id, + blocks => [{ip=>'10.0.4.7',mask=>26}, #0..63 + {ip=>'10.0.4.99',mask=>26}, #64..127 + {ip=>'10.0.5.9',mask=>24}, + {ip=>'10.0.6.9',mask=>24},], + })); + $res = $ua->request($req); + is($res->code, 201, "POST test billingnetwork B"); + $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); + $res = $ua->request($req); + is($res->code, 200, "fetch POSTed billingnetwork B"); + return JSON::from_json($res->decoded_content); +} + +sub _create_base_profile_package { + + my ($profile_base_any,$profile_base_a,$profile_base_b,$network_a,$network_b) = @_; + $req = HTTP::Request->new('POST', $uri.'/api/profilepackages/'); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + #$req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + #my $name = $start_mode . ($interval_unit ? '/' . $interval_value . ' ' . $interval_unit : ''); + $req->content(JSON::to_json({ + name => "base profile package " . $t, + description => "base test profile package description " . $t, + reseller_id => $default_reseller_id, + initial_profiles => [{ profile_id => $profile_base_any->{id}, }, + { profile_id => $profile_base_a->{id}, network_id => $network_a->{id} }, + { profile_id => $profile_base_b->{id}, network_id => $network_b->{id} }], + balance_interval_start_mode => 'topup', + balance_interval_value => 1, + balance_interval_unit => 'month', + carry_over_mode => 'carry_over_timely', + timely_duration_value => 1, + timely_duration_unit => 'month', + })); + $res = $ua->request($req); + is($res->code, 201, "POST test base profilepackage"); + my $profilepackage_uri = $uri.'/'.$res->header('Location'); + $req = HTTP::Request->new('GET', $profilepackage_uri); + $req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + $res = $ua->request($req); + is($res->code, 200, "fetch POSTed base profilepackage"); + my $package = JSON::from_json($res->decoded_content); + $package_map->{$package->{id}} = $package; + return $package; + +} + +sub _create_silver_profile_package { + + my ($base_package,$profile_silver_a,$profile_silver_b,$network_a,$network_b) = @_; + $req = HTTP::Request->new('POST', $uri.'/api/profilepackages/'); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + #$req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + #my $name = $start_mode . ($interval_unit ? '/' . $interval_value . ' ' . $interval_unit : ''); + $req->content(JSON::to_json({ + name => "silver profile package " . $t, + description => "silver test profile package description " . $t, + reseller_id => $default_reseller_id, + initial_profiles => $base_package->{initial_profiles}, + balance_interval_start_mode => 'topup', + balance_interval_value => 1, + balance_interval_unit => 'month', + carry_over_mode => 'carry_over_timely', + timely_duration_value => 1, + timely_duration_unit => 'month', + + service_charge => 200, + topup_profiles => [ #{ profile_id => $profile_silver_any->{id}, }, + { profile_id => $profile_silver_a->{id}, network_id => $network_a->{id} } , + { profile_id => $profile_silver_b->{id}, network_id => $network_b->{id} } ], + })); + $res = $ua->request($req); + is($res->code, 201, "POST test silver profilepackage"); + my $profilepackage_uri = $uri.'/'.$res->header('Location'); + $req = HTTP::Request->new('GET', $profilepackage_uri); + $req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + $res = $ua->request($req); + is($res->code, 200, "fetch POSTed silver profilepackage"); + my $package = JSON::from_json($res->decoded_content); + $package_map->{$package->{id}} = $package; + return $package; + +} + +sub _create_extension_profile_package { + + my ($base_package,$profile_silver_a,$profile_silver_b,$network_a,$network_b) = @_; + $req = HTTP::Request->new('POST', $uri.'/api/profilepackages/'); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + #$req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + #my $name = $start_mode . ($interval_unit ? '/' . $interval_value . ' ' . $interval_unit : ''); + $req->content(JSON::to_json({ + name => "extension profile package " . $t, + description => "extension test profile package description " . $t, + reseller_id => $default_reseller_id, + initial_profiles => $base_package->{initial_profiles}, + balance_interval_start_mode => 'topup', + balance_interval_value => 1, + balance_interval_unit => 'month', + carry_over_mode => 'carry_over_timely', + timely_duration_value => 1, + timely_duration_unit => 'month', + + service_charge => 200, + topup_profiles => [ #{ profile_id => $profile_silver_any->{id}, }, + { profile_id => $profile_silver_a->{id}, network_id => $network_a->{id} } , + { profile_id => $profile_silver_b->{id}, network_id => $network_b->{id} } ], + })); + $res = $ua->request($req); + is($res->code, 201, "POST test extension profilepackage"); + my $profilepackage_uri = $uri.'/'.$res->header('Location'); + $req = HTTP::Request->new('GET', $profilepackage_uri); + $req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + $res = $ua->request($req); + is($res->code, 200, "fetch POSTed extension profilepackage"); + my $package = JSON::from_json($res->decoded_content); + $package_map->{$package->{id}} = $package; + return $package; + +} + +sub _create_gold_profile_package { + + my ($base_package,$profile_gold_a,$profile_gold_b,$network_a,$network_b) = @_; + $req = HTTP::Request->new('POST', $uri.'/api/profilepackages/'); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + #$req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + #my $name = $start_mode . ($interval_unit ? '/' . $interval_value . ' ' . $interval_unit : ''); + $req->content(JSON::to_json({ + name => "gold profile package " . $t, + description => "gold test profile package description " . $t, + reseller_id => $default_reseller_id, + initial_profiles => $base_package->{initial_profiles}, + balance_interval_start_mode => 'topup', + balance_interval_value => 1, + balance_interval_unit => 'month', + carry_over_mode => 'carry_over', + #timely_duration_value => 1, + #timely_duration_unit => 'month', + + service_charge => 500, + topup_profiles => [ #{ profile_id => $profile_gold_any->{id}, }, + { profile_id => $profile_gold_a->{id}, network_id => $network_a->{id} } , + { profile_id => $profile_gold_b->{id}, network_id => $network_b->{id} } ], + })); + $res = $ua->request($req); + is($res->code, 201, "POST test gold profilepackage"); + my $profilepackage_uri = $uri.'/'.$res->header('Location'); + $req = HTTP::Request->new('GET', $profilepackage_uri); + $req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + $res = $ua->request($req); + is($res->code, 200, "fetch POSTed gold profilepackage"); + my $package = JSON::from_json($res->decoded_content); + $package_map->{$package->{id}} = $package; + return $package; + +} + +sub _create_voucher { + + my ($amount,$code,$customer,$package,$valid_until_dt) = @_; + my $dtf = DateTime::Format::Strptime->new( + pattern => '%F %T', + ); + $req = HTTP::Request->new('POST', $uri.'/api/vouchers/'); + $req->header('Content-Type' => 'application/json'); + $req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + $req->content(JSON::to_json({ + amount => $amount * 100.0, + code => $code, + customer_id => ($customer ? $customer->{id} : undef), + package_id => ($package ? $package->{id} : undef), + reseller_id => $default_reseller_id, + valid_until => $dtf->format_datetime($valid_until_dt ? $valid_until_dt : NGCP::Panel::Utils::DateTime::current_local->add(years => 1)), + })); + $res = $ua->request($req); + my $label = 'test voucher (' . ($customer ? 'for customer ' . $customer->{id} : 'no customer') . ', ' . ($package ? 'for package ' . $package->{id} : 'no package') . ')'; + is($res->code, 201, "create " . $label); + $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); + $req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + $res = $ua->request($req); + is($res->code, 200, "fetch " . $label); + my $voucher = JSON::from_json($res->decoded_content); + $voucher_map->{$voucher->{id}} = $voucher; + return $voucher; + +} + +sub _create_subscriber { + my ($customer) = @_; + $req = HTTP::Request->new('POST', $uri.'/api/subscribers/'); + $req->header('Content-Type' => 'application/json'); + $req->content(JSON::to_json({ + domain_id => $domain->{id}, + username => 'test_customer_subscriber_' . (scalar keys %$subscriber_map) . '_'.$t, + password => 'test_customer_subscriber_password', + customer_id => $customer->{id}, + #status => "active", + })); + $res = $ua->request($req); + is($res->code, 201, "POST test subscriber"); + $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); + $res = $ua->request($req); + is($res->code, 200, "fetch POSTed test subscriber"); + my $subscriber = JSON::from_json($res->decoded_content); + $subscriber_map->{$subscriber->{id}} = $subscriber; + return $subscriber; +} + +sub _perform_topup_voucher { + + my ($subscriber,$voucher) = @_; + $req = HTTP::Request->new('POST', $uri.'/api/topupvouchers/'); + $req->header('Content-Type' => 'application/json'); + $req->header('X-Fake-Clienttime' => _get_rfc_1123_now()); + $req->content(JSON::to_json({ + code => $voucher->{code}, + subscriber_id => $subscriber->{id}, + })); + $res = $ua->request($req); + is($res->code, 204, "perform topup with voucher " . $voucher->{code}); + +} + +sub _create_billing_profile { + my ($name) = @_; + $req = HTTP::Request->new('POST', $uri.'/api/billingprofiles/'); + $req->header('Content-Type' => 'application/json'); + $req->header('Prefer' => 'return=representation'); + $req->content(JSON::to_json({ + name => $name." $t", + handle => $name."_$t", + reseller_id => $default_reseller_id, + })); + $res = $ua->request($req); + is($res->code, 201, "POST test billing profile " . $name); + $req = HTTP::Request->new('GET', $uri.'/'.$res->header('Location')); + $res = $ua->request($req); + is($res->code, 200, "fetch POSTed billing profile" . $name); + my $billingprofile = JSON::from_json($res->decoded_content); + $profile_map->{$billingprofile->{id}} = $billingprofile; + return $billingprofile; +} + + sub _get_allow_delay_commit { my $allow_delay_commit = 0; my $cfg = $config{api_debug_opts};