From a56100b30ff9acadfec579980fea8a28e3f3527a Mon Sep 17 00:00:00 2001 From: Rene Krenn Date: Thu, 12 Apr 2018 16:44:55 +0200 Subject: [PATCH] TT#35662 move product_id to billing.contracts Change-Id: I5381b36e226b1c0c2032fa40c3a082b85485e4ac (cherry picked from commit b35b0e1cbc5c3254f81a705e67265c337ad86e52) (cherry picked from commit 2492ef27673e510dbed03dda8c751ef9833a06a6) (cherry picked from commit b7e82fdf1e93464d9ecd7c2b7cc47a82649e9ea3) --- lib/NGCP/Panel/Controller/API/Contracts.pm | 8 +- .../Panel/Controller/API/ContractsItem.pm | 17 +- lib/NGCP/Panel/Controller/API/Customers.pm | 9 +- .../Panel/Controller/API/CustomersItem.pm | 6 +- lib/NGCP/Panel/Controller/Billing.pm | 2 +- lib/NGCP/Panel/Controller/Contract.pm | 75 ++- lib/NGCP/Panel/Controller/Customer.pm | 85 +-- lib/NGCP/Panel/Controller/Reseller.pm | 53 +- lib/NGCP/Panel/Controller/Sound.pm | 60 +- lib/NGCP/Panel/Controller/Subscriber.pm | 2 +- lib/NGCP/Panel/Field/Contract.pm | 2 +- lib/NGCP/Panel/Form/Contract/Base.pm | 5 +- lib/NGCP/Panel/Form/Contract/ProductSelect.pm | 2 +- lib/NGCP/Panel/Role/API.pm | 10 +- lib/NGCP/Panel/Role/API/Admins.pm | 1 + lib/NGCP/Panel/Role/API/BalanceIntervals.pm | 4 +- lib/NGCP/Panel/Role/API/BillingProfiles.pm | 2 +- lib/NGCP/Panel/Role/API/Capabilities.pm | 2 +- lib/NGCP/Panel/Role/API/Contracts.pm | 29 +- lib/NGCP/Panel/Role/API/Customers.pm | 23 +- lib/NGCP/Panel/Role/API/Subscribers.pm | 21 +- lib/NGCP/Panel/Utils/Billing.pm | 2 + lib/NGCP/Panel/Utils/BillingMappings.pm | 542 +++++++++++++++++ lib/NGCP/Panel/Utils/Contract.pm | 563 +----------------- lib/NGCP/Panel/Utils/Invoice.pm | 58 +- lib/NGCP/Panel/Utils/ProfilePackages.pm | 54 +- share/templates/helpers/datatables.tt | 15 +- 27 files changed, 847 insertions(+), 805 deletions(-) create mode 100644 lib/NGCP/Panel/Utils/BillingMappings.pm diff --git a/lib/NGCP/Panel/Controller/API/Contracts.pm b/lib/NGCP/Panel/Controller/API/Contracts.pm index 56faa3fda7..3d81580a2e 100644 --- a/lib/NGCP/Panel/Controller/API/Contracts.pm +++ b/lib/NGCP/Panel/Controller/API/Contracts.pm @@ -12,11 +12,14 @@ use HTTP::Status qw(:constants); use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::Contract; use NGCP::Panel::Utils::ProfilePackages qw(); + require Catalyst::ActionRole::ACL; require Catalyst::ActionRole::CheckTrailingSlash; require NGCP::Panel::Role::HTTPMethods; require Catalyst::ActionRole::RequireSSL; +use NGCP::Panel::Utils::BillingMappings qw(); + sub allowed_methods{ return [qw/GET POST OPTIONS HEAD/]; } @@ -203,7 +206,7 @@ sub POST :Allow { } my $mappings_to_create = []; - last unless NGCP::Panel::Utils::Contract::prepare_billing_mappings( + last unless NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $resource, old_resource => undef, @@ -215,11 +218,12 @@ sub POST :Allow { }); my $product_class = delete $resource->{type}; - my $product = $schema->resultset('products')->find({ class => $product_class }); + my $product = $schema->resultset('products')->search_rs({ class => $product_class })->first; unless($product) { $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'type'."); last; } + $resource->{product_id} = $product->id; my $now = NGCP::Panel::Utils::DateTime::current_local; $resource->{create_timestamp} = $now; diff --git a/lib/NGCP/Panel/Controller/API/ContractsItem.pm b/lib/NGCP/Panel/Controller/API/ContractsItem.pm index 66425c2b1b..c5bbf9e416 100644 --- a/lib/NGCP/Panel/Controller/API/ContractsItem.pm +++ b/lib/NGCP/Panel/Controller/API/ContractsItem.pm @@ -12,6 +12,7 @@ use HTTP::Status qw(:constants); use NGCP::Panel::Utils::ValidateJSON qw(); use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::Contract qw(); +use NGCP::Panel::Utils::BillingMappings qw(); require Catalyst::ActionRole::ACL; require NGCP::Panel::Role::HTTPMethods; require Catalyst::ActionRole::RequireSSL; @@ -123,7 +124,7 @@ sub PATCH :Allow { last unless $preference; my $json = $self->get_valid_patch_data( - c => $c, + c => $c, id => $id, media_type => 'application/json-patch+json', ); @@ -132,9 +133,10 @@ sub PATCH :Allow { my $now = NGCP::Panel::Utils::DateTime::current_local; my $contract = $self->contract_by_id($c, $id, $now); last unless $self->resource_exists($c, contract => $contract); - + my $old_resource = { $contract->get_inflated_columns }; - my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid')); + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, ); + #my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid')); $old_resource->{billing_profile_id} = $billing_mapping->billing_profile_id; $old_resource->{billing_profile_definition} = undef; delete $old_resource->{profile_package_id}; @@ -142,7 +144,7 @@ sub PATCH :Allow { my $resource = $self->apply_patch($c, $old_resource, $json, sub { my ($missing_field,$entity) = @_; if ($missing_field eq 'billing_profiles') { - $entity->{billing_profiles} = NGCP::Panel::Utils::Contract::resource_from_future_mappings($contract); + $entity->{billing_profiles} = NGCP::Panel::Utils::BillingMappings::resource_from_future_mappings($contract); $entity->{billing_profile_definition} //= 'profiles'; } }); @@ -154,7 +156,7 @@ sub PATCH :Allow { my $hal = $self->hal_from_contract($c, $contract, $form, $now); last unless $self->add_update_journal_item_hal($c,$hal); - + $guard->commit; if ('minimal' eq $preference) { @@ -192,13 +194,14 @@ sub PUT :Allow { ); last unless $resource; my $old_resource = { $contract->get_inflated_columns }; - my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid')); + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, ); + #my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid')); $old_resource->{type} = $billing_mapping->product->class; my $form = $self->get_form($c); $contract = $self->update_contract($c, $contract, $old_resource, $resource, $form, $now); last unless $contract; - + my $hal = $self->hal_from_contract($c, $contract, $form, $now); last unless $self->add_update_journal_item_hal($c,$hal); diff --git a/lib/NGCP/Panel/Controller/API/Customers.pm b/lib/NGCP/Panel/Controller/API/Customers.pm index 880b7fdf1e..f1832bb63d 100644 --- a/lib/NGCP/Panel/Controller/API/Customers.pm +++ b/lib/NGCP/Panel/Controller/API/Customers.pm @@ -12,11 +12,14 @@ use HTTP::Status qw(:constants); use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::Contract; use NGCP::Panel::Utils::ProfilePackages qw(); + require Catalyst::ActionRole::ACL; require Catalyst::ActionRole::CheckTrailingSlash; require NGCP::Panel::Role::HTTPMethods; require Catalyst::ActionRole::RequireSSL; +use NGCP::Panel::Utils::BillingMappings qw(); + sub allowed_methods{ return [qw/GET POST OPTIONS HEAD/]; } @@ -276,7 +279,7 @@ sub POST :Allow { } my $mappings_to_create = []; - last unless NGCP::Panel::Utils::Contract::prepare_billing_mappings( + last unless NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $resource, old_resource => undef, @@ -288,11 +291,12 @@ sub POST :Allow { }); my $product_class = delete $resource->{type}; - my $product = $schema->resultset('products')->find({ class => $product_class }); + my $product = $schema->resultset('products')->search_rs({ class => $product_class })->first; unless($product) { $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'type'."); last; } + $resource->{product_id} = $product->id; my $now = NGCP::Panel::Utils::DateTime::current_local; $resource->{create_timestamp} = $now; @@ -301,6 +305,7 @@ sub POST :Allow { try { $customer = $schema->resultset('contracts')->create($resource); + $c->log->debug("customer id " . $customer->id . " created"); } catch($e) { $c->log->error("failed to create customer contract: $e"); # TODO: user, message, trace, ... $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create customer."); diff --git a/lib/NGCP/Panel/Controller/API/CustomersItem.pm b/lib/NGCP/Panel/Controller/API/CustomersItem.pm index 1760fd2592..afe2735ee5 100644 --- a/lib/NGCP/Panel/Controller/API/CustomersItem.pm +++ b/lib/NGCP/Panel/Controller/API/CustomersItem.pm @@ -12,6 +12,7 @@ use HTTP::Status qw(:constants); use NGCP::Panel::Utils::ValidateJSON qw(); use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::Contract qw(); +use NGCP::Panel::Utils::BillingMappings qw(); require Catalyst::ActionRole::ACL; require NGCP::Panel::Role::HTTPMethods; require Catalyst::ActionRole::RequireSSL; @@ -141,14 +142,15 @@ sub PATCH :Allow { my $old_resource = { $customer->get_inflated_columns }; delete $old_resource->{profile_package_id}; - my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid')); + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, ); + #my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid')); $old_resource->{billing_profile_id} = $billing_mapping->billing_profile_id; $old_resource->{billing_profile_definition} = undef; my $resource = $self->apply_patch($c, $old_resource, $json, sub { my ($missing_field,$entity) = @_; if ($missing_field eq 'billing_profiles') { - $entity->{billing_profiles} = NGCP::Panel::Utils::Contract::resource_from_future_mappings($customer); + $entity->{billing_profiles} = NGCP::Panel::Utils::BillingMappings::resource_from_future_mappings($customer); $entity->{billing_profile_definition} //= 'profiles'; } elsif ($missing_field eq 'profile_package_id') { $entity->{profile_package_id} = $customer->profile_package_id; diff --git a/lib/NGCP/Panel/Controller/Billing.pm b/lib/NGCP/Panel/Controller/Billing.pm index 489967540d..62beb5cf4d 100644 --- a/lib/NGCP/Panel/Controller/Billing.pm +++ b/lib/NGCP/Panel/Controller/Billing.pm @@ -175,7 +175,7 @@ sub process_edit :Private { profile_id => $c->stash->{profile_result}->id, old_prepaid => $old_prepaid, new_prepaid => $c->stash->{profile_result}->prepaid, - contract_rs => NGCP::Panel::Utils::Contract::get_contract_rs(schema => $schema), + contract_rs => NGCP::Panel::Utils::Contract::get_contract_rs(schema => $schema), #ok ); }); diff --git a/lib/NGCP/Panel/Controller/Contract.pm b/lib/NGCP/Panel/Controller/Contract.pm index c5d1f7b079..78f8b1ff00 100644 --- a/lib/NGCP/Panel/Controller/Contract.pm +++ b/lib/NGCP/Panel/Controller/Contract.pm @@ -11,6 +11,7 @@ use NGCP::Panel::Utils::Contract; use NGCP::Panel::Utils::ProfilePackages; use NGCP::Panel::Utils::Subscriber; use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::BillingMappings qw(); sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) { my ($self, $c) = @_; @@ -22,16 +23,17 @@ sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) { sub contract_list :Chained('/') :PathPart('contract') :CaptureArgs(0) { my ($self, $c) = @_; + my $now = NGCP::Panel::Utils::DateTime::current_local; $c->stash->{contract_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => "id", search => 1, title => $c->loc("#") }, { name => "external_id", search => 1, title => $c->loc("External #") }, { name => "contact.email", search => 1, title => $c->loc("Contact Email") }, - { name => "billing_mappings_actual.billing_mappings.product.name", search => 1, title => $c->loc("Product") }, - { name => "billing_mappings_actual.billing_mappings.billing_profile.name", search => 1, title => $c->loc("Billing Profile") }, + { name => "product.name", search => 1, title => $c->loc("Product") }, + { name => 'billing_profile_name', accessor => "billing_profile_name", search => 0, title => $c->loc('Billing Profile'), + literal_sql => NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping_stmt(c => $c, now => $now, projection => 'billing_profile.name' ) }, { name => "status", search => 1, title => $c->loc("Status") }, ]); - my $now = NGCP::Panel::Utils::DateTime::current_local; my $rs = NGCP::Panel::Utils::Contract::get_contract_rs( schema => $c->model('DB'), now => $now @@ -81,17 +83,11 @@ sub base :Chained('contract_list') :PathPart('') :CaptureArgs(1) { my $contract_rs = $c->stash->{contract_select_rs} ->search({ 'me.id' => $contract_id, - },{ - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - }); + },undef); my $contract_terminated_rs = $c->stash->{contract_select_all_rs} ->search({ 'me.id' => $contract_id, - },{ - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - }); + },undef); my $contract_first = $contract_rs->first; @@ -104,16 +100,16 @@ sub base :Chained('contract_list') :PathPart('') :CaptureArgs(1) { NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/contract')); } - my $billing_mapping = $contract_first->billing_mappings->find($contract_first->get_column('bmid')); + my $now = $c->stash->{now}; + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract_first, now => $now, ); if (! defined ($billing_mapping->product) || ( $billing_mapping->product->handle ne 'VOIP_RESELLER' && $billing_mapping->product->handle ne 'SIP_PEERING' && $billing_mapping->product->handle ne 'PSTN_PEERING')) { } - my $now = $c->stash->{now}; - my $billing_mappings_ordered = NGCP::Panel::Utils::Contract::billing_mappings_ordered($contract_rs->first->billing_mappings,$now,$contract_first->get_column('bmid')); - my $future_billing_mappings = NGCP::Panel::Utils::Contract::billing_mappings_ordered(NGCP::Panel::Utils::Contract::future_billing_mappings($contract_rs->first->billing_mappings,$now)); + my $billing_mappings_ordered = NGCP::Panel::Utils::BillingMappings::billing_mappings_ordered($contract_first->billing_mappings,$now,$billing_mapping->id); + my $future_billing_mappings = NGCP::Panel::Utils::BillingMappings::billing_mappings_ordered(NGCP::Panel::Utils::BillingMappings::future_billing_mappings($contract_first->billing_mappings,$now)); $c->stash(contract => $contract_first); $c->stash(contract_rs => $contract_rs); @@ -144,8 +140,8 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) { } $params = merge($params, $c->session->{created_objects}); my ($form, $is_peering_reseller); - if (defined $billing_mapping->product && - grep {$billing_mapping->product->handle eq $_} + if (defined $contract->product && + grep {$contract->product->handle eq $_} ("SIP_PEERING", "PSTN_PEERING", "VOIP_RESELLER") ) { $form = NGCP::Panel::Form::get("NGCP::Panel::Form::Contract::PeeringReseller", $c); $is_peering_reseller = 1; @@ -186,7 +182,7 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) { my $mappings_to_create = []; my $delete_mappings = 0; my $set_package = ($form->values->{billing_profile_definition} // 'id') eq 'package'; - NGCP::Panel::Utils::Contract::prepare_billing_mappings( + NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $form->values, old_resource => { $contract->get_inflated_columns }, @@ -202,11 +198,11 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) { my $old_package = $contract->profile_package; $contract->update($form->values); - NGCP::Panel::Utils::Contract::remove_future_billing_mappings($contract,$now) if $delete_mappings; + NGCP::Panel::Utils::BillingMappings::remove_future_billing_mappings($contract,$now) if $delete_mappings; foreach my $mapping (@$mappings_to_create) { $contract->billing_mappings->create($mapping); } - $contract = $c->stash->{contract_terminated_rs}->first; + #$contract = $c->stash->{contract_terminated_rs}->first; my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c, contract => $contract, @@ -316,7 +312,7 @@ sub billingmappings_ajax :Chained('base') :PathPart('billingmappings/ajax') :Arg my ($self, $c) = @_; $c->stash(timeline_data => { contract => { $c->stash->{contract}->get_columns }, - events => NGCP::Panel::Utils::Contract::get_billingmappings_timeline_data($c,$c->stash->{contract}), + events => NGCP::Panel::Utils::BillingMappings::get_billingmappings_timeline_data($c,$c->stash->{contract}), }); $c->detach( $c->view("JSON") ); } @@ -354,6 +350,7 @@ sub peering_create :Chained('peering_list') :PathPart('create') :Args(0) { delete $params->{contact}; } $c->stash->{type} = 'sippeering'; + $c->stash(ajax_uri => $c->uri_for_action("/contract/ajax")); my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::Contract::PeeringReseller", $c); $form->process( posted => $posted, @@ -378,9 +375,10 @@ sub peering_create :Chained('peering_list') :PathPart('create') :Args(0) { } $form->values->{external_id} = $form->field('external_id')->value; $form->values->{create_timestamp} = $form->values->{modify_timestamp} = NGCP::Panel::Utils::DateTime::current_local; + $form->values->{product_id} = $schema->resultset('products')->search_rs({ class => $c->stash->{type} })->first->id; my $mappings_to_create = []; - NGCP::Panel::Utils::Contract::prepare_billing_mappings( + NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $form->values, mappings_to_create => $mappings_to_create, @@ -393,13 +391,10 @@ sub peering_create :Chained('peering_list') :PathPart('create') :Args(0) { foreach my $mapping (@$mappings_to_create) { $contract->billing_mappings->create($mapping); } - $contract = $c->stash->{contract_select_rs} - ->search({ - 'me.id' => $contract->id, - },{ - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - })->first; + #$contract = $c->stash->{contract_select_rs} + # ->search({ + # 'me.id' => $contract->id, + # },undef)->first; NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balances(c => $c, contract => $contract, @@ -478,16 +473,15 @@ sub reseller_ajax_contract_filter :Chained('reseller_list') :PathPart('ajax/cont my $now = $c->stash->{now}; my $rs = NGCP::Panel::Utils::Contract::get_contract_rs( schema => $c->model('DB'), - now => $now, - contract_id => $contract_id ) - ->search_rs({ + now => $now)->search_rs({ 'me.id' => $contract_id, }); my $contract_columns = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => "id", search => 1, title => $c->loc("#") }, { name => "external_id", search => 1, title => $c->loc("External #") }, { name => "contact.email", search => 1, title => $c->loc("Contact Email") }, - { name => "billing_mappings_actual.billing_mappings.billing_profile.name", search => 1, title => $c->loc("Billing Profile") }, + { name => 'billing_profile_name', accessor => "billing_profile_name", search => 0, title => $c->loc('Billing Profile'), + literal_sql => NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping_stmt(c => $c, now => $now, projection => 'billing_profile.name' ) }, { name => "status", search => 1, title => $c->loc("Status") }, ]); NGCP::Panel::Utils::Datatables::process($c, $rs, $contract_columns); @@ -504,6 +498,7 @@ sub reseller_create :Chained('reseller_list') :PathPart('create') :Args(0) { delete $params->{contact}; } $c->stash->{type} = 'reseller'; + $c->stash(ajax_uri => $c->uri_for_action("/contract/ajax")); my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::Contract::PeeringReseller", $c); $form->process( posted => $posted, @@ -529,9 +524,10 @@ sub reseller_create :Chained('reseller_list') :PathPart('create') :Args(0) { } $form->values->{external_id} = $form->field('external_id')->value; $form->values->{create_timestamp} = $form->values->{modify_timestamp} = NGCP::Panel::Utils::DateTime::current_local; + $form->values->{product_id} = $schema->resultset('products')->search_rs({ class => $c->stash->{type} })->first->id; my $mappings_to_create = []; - NGCP::Panel::Utils::Contract::prepare_billing_mappings( + NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $form->values, mappings_to_create => $mappings_to_create, @@ -544,13 +540,10 @@ sub reseller_create :Chained('reseller_list') :PathPart('create') :Args(0) { foreach my $mapping (@$mappings_to_create) { $contract->billing_mappings->create($mapping); } - $contract = $c->stash->{contract_select_rs} - ->search({ - 'me.id' => $contract->id, - },{ - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - })->first; + #$contract = $c->stash->{contract_select_rs} + # ->search({ + # 'me.id' => $contract->id, + # },undef)->first; NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balances(c => $c, contract => $contract, diff --git a/lib/NGCP/Panel/Controller/Customer.pm b/lib/NGCP/Panel/Controller/Customer.pm index 3dcb37eb77..28aa98c19c 100644 --- a/lib/NGCP/Panel/Controller/Customer.pm +++ b/lib/NGCP/Panel/Controller/Customer.pm @@ -13,6 +13,7 @@ use NGCP::Panel::Utils::Subscriber; use NGCP::Panel::Utils::Sounds; use NGCP::Panel::Utils::Contract; use NGCP::Panel::Utils::ProfilePackages; +use NGCP::Panel::Utils::BillingMappings qw(); use NGCP::Panel::Utils::DeviceBootstrap; use NGCP::Panel::Utils::Voucher; use NGCP::Panel::Utils::ContractLocations qw(); @@ -41,6 +42,7 @@ sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) :AllowedRol sub list_customer :Chained('/') :PathPart('customer') :CaptureArgs(0) { my ($self, $c) = @_; + my $now = NGCP::Panel::Utils::DateTime::current_local; $c->stash->{contract_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => "id", search => 1, title => $c->loc("#") }, { name => "external_id", search => 1, title => $c->loc("External #") }, @@ -49,17 +51,17 @@ sub list_customer :Chained('/') :PathPart('customer') :CaptureArgs(0) { { name => "contact.firstname", search => 1, title => '' }, { name => "contact.lastname", search => 1, title => $c->loc("Name"), custom_renderer => 'function ( data, type, full ) { var sep = (full.contact_firstname && full.contact_lastname) ? " " : ""; return (full.contact_firstname || "") + sep + (full.contact_lastname || ""); }' }, - { name => "billing_mappings_actual.billing_mappings.product.name", search => 1, title => $c->loc("Product") }, - { name => "billing_mappings_actual.billing_mappings.billing_profile.name", search => 1, title => $c->loc("Billing Profile") }, + { name => "product.name", search => 1, title => $c->loc("Product") }, + { name => 'billing_profile_name', accessor => "billing_profile_name", search => 0, title => $c->loc('Billing Profile'), + literal_sql => '""' }, { name => "status", search => 1, title => $c->loc("Status") }, { name => "max_subscribers", search => 1, title => $c->loc("Max. Subscribers") }, ]); - my $now = NGCP::Panel::Utils::DateTime::current_local; - my $rs = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c, now => $now); + my $rs = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c); #, now => $now); my $rs_all = NGCP::Panel::Utils::Contract::get_contract_rs( schema => $c->model('DB'), - now => $now, + #now => $now, include_terminated => 1, ); $c->stash( @@ -76,11 +78,14 @@ sub root :Chained('list_customer') :PathPart('') :Args(0) :Does(ACL) :ACLDetachT sub ajax :Chained('list_customer') :PathPart('ajax') :Args(0) { my ($self, $c) = @_; + my $now = NGCP::Panel::Utils::DateTime::current_local; #uniform ts my $res = $c->stash->{contract_select_rs}; NGCP::Panel::Utils::Datatables::process($c, $res, $c->stash->{contract_dt_columns}, sub { my $item = shift; my %contact = $item->contact->get_inflated_columns; my %result = map { (ref $contact{$_}) ? () : ('contact_'.$_ => $contact{$_}); } keys %contact; + my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $item, now => $now, ); + $result{'billing_profile_name'} = $bm_actual->billing_profile->name if $bm_actual; return %result; },); $c->detach( $c->view("JSON") ); @@ -107,7 +112,7 @@ sub ajax_reseller_filter :Chained('list_customer') :PathPart('ajax/reseller') :A my $reseller_customer_columns = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => "id", search => 1, title => $c->loc("#") }, { name => "external_id", search => 1, title => $c->loc("External #") }, - { name => "billing_mappings_actual.billing_mappings.product.name", search => 1, title => $c->loc("Product") }, + { name => "product.name", search => 1, title => $c->loc("Product") }, { name => "contact.email", search => 1, title => $c->loc("Contact Email") }, { name => "status", search => 1, title => $c->loc("Status") }, ]); @@ -140,11 +145,14 @@ sub ajax_package_filter :Chained('list_customer') :PathPart('ajax/package') :Arg sub ajax_pbx_only :Chained('list_customer') :PathPart('ajax_pbx_only') :Args(0) { my ($self, $c) = @_; + my $now = NGCP::Panel::Utils::DateTime::current_local; #uniform ts my $res = $c->stash->{contract_select_rs}->search_rs({'product.class' => 'pbxaccount'}); NGCP::Panel::Utils::Datatables::process($c, $res, $c->stash->{contract_dt_columns}, sub { my $item = shift; my %contact = $item->contact->get_inflated_columns; my %result = map { (ref $contact{$_}) ? () : ('contact_'.$_ => $contact{$_}); } keys %contact; + my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $item, now => $now, ); + $result{'billing_profile_name'} = $bm_actual->billing_profile->name if $bm_actual; return %result; },); $c->detach( $c->view("JSON") ); @@ -201,12 +209,13 @@ sub create :Chained('list_customer') :PathPart('create') :Args(0) { #$form->values->{profile_package_id} = undef unless NGCP::Panel::Utils::ProfilePackages::ENABLE_PROFILE_PACKAGES; $form->values->{create_timestamp} = $form->values->{modify_timestamp} = NGCP::Panel::Utils::DateTime::current_local; $form->values->{external_id} = $form->field('external_id')->value; + $form->values->{product_id} //= $schema->resultset('products')->search_rs({ class => $c->stash->{type} })->first->id; unless($form->values->{max_subscribers} && length($form->values->{max_subscribers})) { delete $form->values->{max_subscribers}; } my $mappings_to_create = []; - NGCP::Panel::Utils::Contract::prepare_billing_mappings( + NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $form->values, mappings_to_create => $mappings_to_create, @@ -214,19 +223,16 @@ sub create :Chained('list_customer') :PathPart('create') :Args(0) { my ($err,@fields) = @_; die( [$err, "showdetails"] ); }); - delete $form->values->{product_id}; + #delete $form->values->{product_id}; my $contract = $schema->resultset('contracts')->create($form->values); foreach my $mapping (@$mappings_to_create) { $contract->billing_mappings->create($mapping); } - $contract = $c->stash->{contract_select_rs} - ->search({ - 'me.id' => $contract->id, - },{ - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - })->first; + #$contract = $c->stash->{contract_select_rs} + # ->search({ + # 'me.id' => $contract->id, + # },undef)->first; NGCP::Panel::Utils::ProfilePackages::create_initial_contract_balances(c => $c, contract => $contract, @@ -278,17 +284,11 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) { my $contract_rs = $c->stash->{contract_select_rs} ->search({ 'me.id' => $contract_id, - },{ - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - }); + },undef); my $contract_terminated_rs = $c->stash->{contract_select_all_rs} ->search({ 'me.id' => $contract_id, - },{ - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - }); + },undef); if($c->user->roles eq 'reseller') { $contract_rs = $contract_rs->search({ @@ -305,7 +305,8 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) { $c->detach('/denied_page'); } } - unless(defined($contract_rs->first)) { + my $contract_first = $contract_rs->first; + unless(defined($contract_first)) { NGCP::Panel::Utils::Message::error( c => $c, log => 'Customer was not found', @@ -314,10 +315,11 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) { NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/customer')); } + my $now = $c->stash->{now}; - my $billing_mappings_ordered = NGCP::Panel::Utils::Contract::billing_mappings_ordered($contract_rs->first->billing_mappings,$now,$contract_rs->first->get_column('bmid')); - my $future_billing_mappings = NGCP::Panel::Utils::Contract::billing_mappings_ordered(NGCP::Panel::Utils::Contract::future_billing_mappings($contract_rs->first->billing_mappings,$now)); - my $billing_mapping = $contract_rs->first->billing_mappings->find($contract_rs->first->get_column('bmid')); + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract_first, now => $now, ); + my $billing_mappings_ordered = NGCP::Panel::Utils::BillingMappings::billing_mappings_ordered($contract_first->billing_mappings,$now,$billing_mapping->id); + my $future_billing_mappings = NGCP::Panel::Utils::BillingMappings::billing_mappings_ordered(NGCP::Panel::Utils::BillingMappings::future_billing_mappings($contract_first->billing_mappings,$now)); my $balance; try { @@ -346,20 +348,20 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) { NGCP::Panel::Utils::ProfilePackages::get_topuplog_datatable_cols($c), ]); - my $product_id = $contract_rs->first->get_column('product_id'); + my $product = $contract_first->product; NGCP::Panel::Utils::Message::error( c => $c, error => "No product for customer contract id $contract_id found", desc => $c->loc('No product for this customer contract found.'), - ) unless($product_id); - - my $product = $c->model('DB')->resultset('products')->find($product_id); - NGCP::Panel::Utils::Message::error( - c => $c, - error => "No product with id $product_id for customer contract id $contract_id found", - desc => $c->loc('Invalid product id for this customer contract.'), ) unless($product); + #my $product = $c->model('DB')->resultset('products')->find($product_id); + #NGCP::Panel::Utils::Message::error( + # c => $c, + # error => "No product with id $product_id for customer contract id $contract_id found", + # desc => $c->loc('Invalid product id for this customer contract.'), + #) unless($product); + # only show the extension if it's a pbx extension. otherwise (and in case of a pilot?) show the # number @@ -407,7 +409,6 @@ sub base :Chained('list_customer') :PathPart('') :CaptureArgs(1) { })->all ]; # contents of details page: - my $contract_first = $contract_rs->first; NGCP::Panel::Utils::Sounds::stash_soundset_list(c => $c, contract => $contract_first); $c->stash->{contact_hash} = { $contract_first->contact->get_inflated_columns }; if(defined $contract_first->max_subscribers) { @@ -534,7 +535,7 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) { my $mappings_to_create = []; my $delete_mappings = 0; my $set_package = ($form->values->{billing_profile_definition} // 'id') eq 'package'; - NGCP::Panel::Utils::Contract::prepare_billing_mappings( + NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $form->values, old_resource => { $contract->get_inflated_columns }, @@ -545,7 +546,7 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) { my ($err,@fields) = @_; die( [$err, "showdetails"] ); }); - delete $form->values->{product_id}; + #delete $form->values->{product_id}; my $old_prepaid = $billing_mapping->billing_profile->prepaid; my $old_ext_id = $contract->external_id // ''; @@ -553,11 +554,11 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) { my $old_package = $contract->profile_package; $contract->update($form->values); - NGCP::Panel::Utils::Contract::remove_future_billing_mappings($contract,$now) if $delete_mappings; + NGCP::Panel::Utils::BillingMappings::remove_future_billing_mappings($contract,$now) if $delete_mappings; foreach my $mapping (@$mappings_to_create) { $contract->billing_mappings->create($mapping); } - $contract = $c->stash->{contract_terminated_rs}->first; + #$contract = $c->stash->{contract_terminated_rs}->first; my $balance = NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c, contract => $contract, @@ -571,7 +572,7 @@ sub edit :Chained('base_restricted') :PathPart('edit') :Args(0) { profiles_added => ($set_package ? scalar @$mappings_to_create : 0), ); - $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid')); + $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(contract => $contract, schema => $schema, now => $now, ); $billing_profile = $billing_mapping->billing_profile; my $new_ext_id = $contract->external_id // ''; @@ -1257,7 +1258,7 @@ sub billingmappings_ajax :Chained('base') :PathPart('billingmappings/ajax') :Arg my ($self, $c) = @_; $c->stash(timeline_data => { contract => { $c->stash->{contract}->get_columns }, - events => NGCP::Panel::Utils::Contract::get_billingmappings_timeline_data($c,$c->stash->{contract}), + events => NGCP::Panel::Utils::BillingMappings::get_billingmappings_timeline_data($c,$c->stash->{contract}), }); $c->detach( $c->view("JSON") ); } diff --git a/lib/NGCP/Panel/Controller/Reseller.pm b/lib/NGCP/Panel/Controller/Reseller.pm index 77c73b9625..b5eb9ae8fa 100644 --- a/lib/NGCP/Panel/Controller/Reseller.pm +++ b/lib/NGCP/Panel/Controller/Reseller.pm @@ -14,6 +14,7 @@ use NGCP::Panel::Utils::Navigation; use NGCP::Panel::Utils::Reseller; use NGCP::Panel::Utils::BillingNetworks qw(); use NGCP::Panel::Utils::ProfilePackages qw(); +use NGCP::Panel::Utils::BillingMappings qw(); use NGCP::Panel::Utils::Billing qw(); use NGCP::Panel::Utils::Admin; @@ -50,7 +51,8 @@ sub list_reseller :Chained('/') :PathPart('reseller') :CaptureArgs(0) { { name => "id", search => 1, title => $c->loc("#") }, { name => "external_id", search => 1, title => $c->loc("External #") }, { name => "contact.email", search => 1, title => $c->loc("Contact Email") }, - { name => "billing_mappings_actual.billing_mappings.billing_profile.name", search => 1, title => $c->loc("Billing Profile") }, + { name => 'billing_profile_name', accessor => "billing_profile_name", search => 0, title => $c->loc('Billing Profile'), + literal_sql => NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping_stmt(c => $c, projection => 'billing_profile.name' ) }, { name => "status", search => 1, title => $c->loc("Status") }, ]); } @@ -93,8 +95,8 @@ sub create :Chained('list_reseller') :PathPart('create') :Args(0) :Does(ACL) :AC item => $params, ); NGCP::Panel::Utils::Navigation::check_form_buttons( - c => $c, - form => $form, + c => $c, + form => $form, fields => {'contract.create' => $c->uri_for('/contract/reseller/create') }, back_uri => $c->req->uri, ); @@ -162,7 +164,7 @@ sub base :Chained('list_reseller') :PathPart('') :CaptureArgs(1) { { name => "firstname", search => 1, title => $c->loc('First Name') }, { name => "lastname", search => 1, title => $c->loc('Last Name') }, { name => "company", search => 1, title => $c->loc('Company') }, - { name => "email", search => 1, title => $c->loc('Email') }, + { name => "email", search => 1, title => $c->loc('Email') }, ]); $c->stash->{reseller_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => "id", search => 1, title => $c->loc('#') }, @@ -196,29 +198,29 @@ sub base :Chained('list_reseller') :PathPart('') :CaptureArgs(1) { { name => 'name', search => 1, title => $c->loc('Name') }, { name => 'type', search => 1, title => $c->loc('Type') }, ]); - + $c->stash->{profile_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => "id", "search" => 1, "title" => $c->loc("#") }, { name => "name", "search" => 1, "title" => $c->loc("Name") }, #{ name => "reseller.name", "search" => 1, "title" => $c->loc("Reseller") }, #{ name => "v_count_used", "search" => 0, "title" => $c->loc("Used") }, NGCP::Panel::Utils::Billing::get_datatable_cols($c), - ]); - + ]); + $c->stash->{network_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => 'id', search => 1, title => $c->loc('#') }, #{ name => 'reseller.name', search => 1, title => $c->loc('Reseller') }, { name => 'name', search => 1, title => $c->loc('Name') }, NGCP::Panel::Utils::BillingNetworks::get_datatable_cols($c), - ]); - + ]); + $c->stash->{package_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ { name => 'id', search => 1, title => $c->loc('#') }, #{ name => 'reseller.name', search => 1, title => $c->loc('Reseller') }, { name => 'name', search => 1, title => $c->loc('Name') }, NGCP::Panel::Utils::ProfilePackages::get_datatable_cols($c), ]); - + $c->stash(reseller => $c->stash->{resellers}->search_rs({ id => $reseller_id })); unless($c->stash->{reseller}->first) { NGCP::Panel::Utils::Message::error( @@ -406,20 +408,18 @@ sub details :Chained('base') :PathPart('details') :Args(0) :Does(ACL) :ACLDetach sub ajax_contract :Chained('list_reseller') :PathPart('ajax_contract') :Args(0) :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) { my ($self, $c) = @_; - + my $edit_contract_id = $c->session->{edit_contract_id}; my @used_contracts = map { unless($edit_contract_id && $edit_contract_id == $_->get_column('contract_id')) { $_->get_column('contract_id') } else {} } $c->stash->{resellers}->all; - my $free_contracts = NGCP::Panel::Utils::Contract::get_contract_rs( - schema => $c->model('DB')) - ->search_rs({ - 'me.status' => { '!=' => 'terminated' }, - 'me.id' => { 'not in' => \@used_contracts }, - 'product.class' => 'reseller', - }); + my $free_contracts = NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB'))->search_rs({ + 'me.status' => { '!=' => 'terminated' }, + 'me.id' => { 'not in' => \@used_contracts }, + 'product.class' => 'reseller', + }); NGCP::Panel::Utils::Datatables::process($c, $free_contracts, $c->stash->{contract_dt_columns}); $c->detach( $c->view("JSON") ); } @@ -433,6 +433,7 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/ my $default_pass = 'defaultresellerpassword'; my $saltedpass = NGCP::Panel::Utils::Admin::generate_salted_hash($default_pass); + my $billing = $c->model('DB'); my $now = NGCP::Panel::Utils::DateTime::current_local; my %defaults = ( contacts => { @@ -445,6 +446,7 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/ status => 'active', create_timestamp => $now, activate_timestamp => $now, + product_id => $billing->resultset('products')->search_rs({ class => 'reseller' })->first->id, }, resellers => { name => 'Default reseller' . sprintf('%04d', rand 10000), @@ -462,7 +464,6 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/ ); $defaults{admins}->{login} = $defaults{resellers}->{name} =~ tr/A-Za-z0-9//cdr; - my $billing = $c->model('DB'); my %r; try { $billing->txn_do(sub { @@ -479,7 +480,8 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/ my $resource = { $r{contracts}->get_inflated_columns }; $resource->{billing_profile_id} = 1; $resource->{type} = 'reseller'; - NGCP::Panel::Utils::Contract::prepare_billing_mappings( + + NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $resource, old_resource => undef, @@ -489,16 +491,11 @@ sub create_defaults :Path('create_defaults') :Args(0) :Does(ACL) :ACLDetachTo('/ die( [$err, "showdetails"] ); }); foreach my $mapping (@$mappings_to_create) { - $r{contracts}->billing_mappings->create($mapping); + $r{contracts}->billing_mappings->create($mapping); } - $r{contracts} = NGCP::Panel::Utils::Contract::get_contract_rs( - schema => $c->model('DB'), - contract_id => $r{contracts}->id )->search(undef, { - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - })->find($r{contracts}->id); + $r{contracts} = NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB'))->find($r{contracts}->id); $r{billing_mappings} = $r{contracts}->billing_mappings; - + $r{admins} = $billing->resultset('admins')->create({ %{ $defaults{admins} }, reseller_id => $r{resellers}->id, diff --git a/lib/NGCP/Panel/Controller/Sound.pm b/lib/NGCP/Panel/Controller/Sound.pm index 9f2211ef16..63f4395580 100644 --- a/lib/NGCP/Panel/Controller/Sound.pm +++ b/lib/NGCP/Panel/Controller/Sound.pm @@ -21,7 +21,7 @@ sub auto :Private { NGCP::Panel::Utils::Navigation::check_redirect_chain(c => $c); # only allow access to admin/reseller if cloudpbx is not enabled - if(!$c->config->{features}->{cloudpbx} && + if(!$c->config->{features}->{cloudpbx} && $c->user->roles ne "admin" && $c->user->roles ne "reseller") { @@ -36,12 +36,10 @@ sub auto :Private { # and then again, it's only for subscriberadmins with pbxaccount product if($c->user->roles eq "subscriberadmin") { my $contract_id = $c->user->account_id; - my $contract_select_rs = NGCP::Panel::Utils::Contract::get_contract_rs( - schema => $c->model('DB'), - contract_id => $contract_id, - ); - $contract_select_rs = $contract_select_rs->search({ 'me.id' => $contract_id }); - my $product_id = $contract_select_rs->first ? $contract_select_rs->first->get_column('product_id') : undef; + my $contract_select_rs = NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB'))->search_rs({ + 'me.id' => $contract_id, + }); + my $product_id = $contract_select_rs->first ? $contract_select_rs->first->product->id : undef; unless($product_id) { NGCP::Panel::Utils::Message::error( c => $c, @@ -50,9 +48,9 @@ sub auto :Private { ); NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/sound')); } - my $product = $c->model('DB')->resultset('products')->find({ + my $product = $c->model('DB')->resultset('products')->search_rs({ id => $product_id, class => 'pbxaccount', - }); + })->first; unless($product) { $c->detach('/denied_page'); } @@ -67,7 +65,7 @@ sub sets_list :Chained('/') :PathPart('sound') :CaptureArgs(0) { if($c->stash->{contract_rs}) { NGCP::Panel::Utils::Sounds::stash_soundset_list( - c => $c, + c => $c, contract => $c->stash->{contract_rs}->first, ); } else { @@ -107,7 +105,7 @@ sub contract_sets_list :Chained('/') :PathPart('sound/contract') :CaptureArgs(1) } NGCP::Panel::Utils::Sounds::stash_soundset_list( - c => $c, + c => $c, contract => $contract, ); $c->stash(template => 'sound/list.tt'); @@ -121,7 +119,7 @@ sub root :Chained('sets_list') :PathPart('') :Args(0) { sub ajax :Chained('sets_list') :PathPart('ajax') :Args(0) { my ($self, $c) = @_; - + my $resultset = $c->stash->{sets_rs}; NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{soundset_dt_columns}); $c->detach( $c->view("JSON") ); @@ -130,7 +128,7 @@ sub ajax :Chained('sets_list') :PathPart('ajax') :Args(0) { sub contract_ajax :Chained('contract_sets_list') :PathPart('ajax') :Args(0) { my ($self, $c) = @_; - + my $resultset = $c->stash->{sets_rs}; NGCP::Panel::Utils::Datatables::process($c, $resultset, $c->stash->{soundset_dt_columns}); $c->detach( $c->view("JSON") ); @@ -229,7 +227,7 @@ sub edit :Chained('base') :PathPart('edit') { my $old_contract_default = $c->stash->{set_result}->contract_default; $c->stash->{set_result}->update($form->values); - if($c->stash->{set_result}->contract && + if($c->stash->{set_result}->contract && $c->stash->{set_result}->contract_default == 1 && $old_contract_default != 1) { # go over each subscriber in the contract and set the contract_sound_set # preference if it doesn't have one set yet @@ -238,7 +236,7 @@ sub edit :Chained('base') :PathPart('edit') { my $prov_subscriber = $bill_subscriber->provisioning_voip_subscriber; if($prov_subscriber) { my $pref_rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( - c => $c, prov_subscriber => $prov_subscriber, attribute => 'contract_sound_set', + c => $c, prov_subscriber => $prov_subscriber, attribute => 'contract_sound_set', ); unless($pref_rs->first) { $pref_rs->create({ value => $c->stash->{set_result}->id }); @@ -281,7 +279,7 @@ sub delete :Chained('base') :PathPart('delete') { # remove all usr_preferenes where this set is assigned if($c->stash->{set_result}->contract_id) { my $pref_rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( - c => $c, attribute => 'contract_sound_set', + c => $c, attribute => 'contract_sound_set', ); $pref_rs->search({ value => $c->stash->{set_result}->id })->delete; } @@ -403,7 +401,7 @@ sub create :Chained('sets_list') :PathPart('create') :Args() { my $prov_subscriber = $bill_subscriber->provisioning_voip_subscriber; if($prov_subscriber) { my $pref_rs = NGCP::Panel::Utils::Preferences::get_usr_preference_rs( - c => $c, prov_subscriber => $prov_subscriber, attribute => 'contract_sound_set', + c => $c, prov_subscriber => $prov_subscriber, attribute => 'contract_sound_set', ); unless($pref_rs->first) { $pref_rs->create({ value => $set->id }); @@ -437,13 +435,13 @@ sub create :Chained('sets_list') :PathPart('create') :Args() { sub handles_list :Chained('base') :PathPart('handles') :CaptureArgs(0) { my ( $self, $c ) = @_; - + my $files_rs = $c->stash->{set_result}->voip_sound_files; $c->stash(files_rs => $files_rs); $c->stash(handles_base_uri => $c->uri_for_action("/sound/handles_root", [$c->req->captures->[0]])); - + my $handles_rs = $c->model('DB')->resultset('voip_sound_groups') ->search({ },{ @@ -464,8 +462,8 @@ sub handles_list :Chained('base') :PathPart('handles') :CaptureArgs(0) { }); if($c->stash->{set_result}->contract_id) { - $handles_rs = $handles_rs->search({ - 'groups.name' => { '-in' => [qw/pbx music_on_hold digits/] } + $handles_rs = $handles_rs->search({ + 'groups.name' => { '-in' => [qw/pbx music_on_hold digits/] } }); } else { #$handles_rs = $handles_rs->search({ 'groups.name' => { '!=' => 'pbx' } }); @@ -484,9 +482,9 @@ sub handles_list :Chained('base') :PathPart('handles') :CaptureArgs(0) { $handles_rs = $handles_rs->search({ 'groups.name' => { '!=' => 'mobile_push' } }); } - + my @rows = $handles_rs->all; - + my %groups; for my $handle (@rows) { $groups{ $handle->get_column('groupname') } = [] @@ -562,12 +560,12 @@ sub handles_edit :Chained('handles_base') :PathPart('edit') { fields => {}, back_uri => $c->req->uri, ); - + if($posted && $form->validated) { if (defined $upload) { my $soundfile = eval { $upload->slurp }; my $filename = eval { $upload->filename }; - + my $ft = File::Type->new(); unless ($ft->checktype_contents($soundfile) eq 'audio/x-wav') { NGCP::Panel::Utils::Message::error( @@ -577,7 +575,7 @@ sub handles_edit :Chained('handles_base') :PathPart('edit') { ); NGCP::Panel::Utils::Navigation::back_or($c, $c->stash->{handles_base_uri}); } - + my $target_codec = 'WAV'; # clear audio caches @@ -604,7 +602,7 @@ sub handles_edit :Chained('handles_base') :PathPart('edit') { ); NGCP::Panel::Utils::Navigation::back_or($c, $c->stash->{handles_base_uri}); } - + try { $file_result->update({ loopplay => $form->values->{loopplay}, @@ -650,7 +648,7 @@ sub handles_edit :Chained('handles_base') :PathPart('edit') { sub handles_delete :Chained('handles_base') :PathPart('delete') { my ($self, $c) = @_; - + try { $c->stash->{file_result}->delete; NGCP::Panel::Utils::Message::info( @@ -681,7 +679,7 @@ sub handles_delete :Chained('handles_base') :PathPart('delete') { sub handles_download :Chained('handles_base') :PathPart('download') :Args(0) { my ($self, $c) = @_; - + my $file = $c->stash->{file_result}; my $filename = $file->filename; $filename =~ s/\.\w+$/.wav/; @@ -702,7 +700,7 @@ sub handles_download :Chained('handles_base') :PathPart('download') :Args(0) { } else { $data = $file->data; } - + $c->response->header ('Content-Disposition' => 'attachment; filename="' . $filename . '"'); $c->response->content_type('audio/x-wav'); $c->response->body($data); @@ -725,7 +723,7 @@ sub handles_load_default :Chained('handles_list') :PathPart('loaddefault') :Args fields => {}, back_uri => $c->req->uri, ); - + if($posted && $form->validated) { my $lang = $form->params->{language}; my $base = "/var/lib/ngcp-soundsets"; diff --git a/lib/NGCP/Panel/Controller/Subscriber.pm b/lib/NGCP/Panel/Controller/Subscriber.pm index c0f6264b20..db27779a70 100644 --- a/lib/NGCP/Panel/Controller/Subscriber.pm +++ b/lib/NGCP/Panel/Controller/Subscriber.pm @@ -141,7 +141,7 @@ sub base :Chained('sub_list') :PathPart('') :CaptureArgs(1) { ); NGCP::Panel::Utils::Navigation::back_or($c, $c->uri_for('/subscriber')); } - my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid')); + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract ); $c->stash->{billing_mapping} = $billing_mapping; $c->stash->{subscribers} = $c->model('DB')->resultset('voip_subscribers')->search({ diff --git a/lib/NGCP/Panel/Field/Contract.pm b/lib/NGCP/Panel/Field/Contract.pm index 80131d537a..7d2a8c79a5 100644 --- a/lib/NGCP/Panel/Field/Contract.pm +++ b/lib/NGCP/Panel/Field/Contract.pm @@ -12,7 +12,7 @@ has_field 'id' => ( template => 'helpers/datatables_field.tt', ajax_src => '/contract/peering/ajax', table_titles => ['#', 'Status', 'Contact Email', 'Billing Profile'], - table_fields => ['id', 'status', 'contact_email', 'billing_mappings_actual_billing_mappings_billing_profile_name'], + table_fields => ['id', 'status', 'contact_email', 'billing_profile_name'], ); has_field 'create' => ( diff --git a/lib/NGCP/Panel/Form/Contract/Base.pm b/lib/NGCP/Panel/Form/Contract/Base.pm index 12c5ef7397..0be54e9909 100644 --- a/lib/NGCP/Panel/Form/Contract/Base.pm +++ b/lib/NGCP/Panel/Form/Contract/Base.pm @@ -6,6 +6,7 @@ extends 'HTML::FormHandler'; use HTML::FormHandler::Widget::Block::Bootstrap; use DateTime::Format::Strptime qw(); use Storable qw(); +use NGCP::Panel::Utils::BillingMappings qw(); with 'NGCP::Panel::Render::RepeatableJs'; @@ -16,7 +17,7 @@ sub build_form_element_class { [qw/form-horizontal/] } has_field 'billing_profile_definition' => ( type => 'Select', - label => 'Set billing profiles', + label => 'Set billing profiles by', options => [ { value => 'id', label => 'single (actual billing mapping)' }, { value => 'profiles', label => 'schedule (billing mapping intervals)' }, @@ -265,7 +266,7 @@ sub validate { my $old_resource = (exists $c->stash->{contract} ? { $c->stash->{contract}->get_inflated_columns } : undef); my $mappings_to_create = []; - NGCP::Panel::Utils::Contract::prepare_billing_mappings( + NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $resource, old_resource => $old_resource, diff --git a/lib/NGCP/Panel/Form/Contract/ProductSelect.pm b/lib/NGCP/Panel/Form/Contract/ProductSelect.pm index 206b2024d9..dff20d0c00 100644 --- a/lib/NGCP/Panel/Form/Contract/ProductSelect.pm +++ b/lib/NGCP/Panel/Form/Contract/ProductSelect.pm @@ -33,7 +33,7 @@ sub validate_max_subscribers { my $product = $form->field('product'); my $max_subscribers = $form->field('max_subscribers'); - my $sipaccount = $c->model('DB')->resultset('products')->find({class => 'sipaccount'}); + my $sipaccount = $c->model('DB')->resultset('products')->search_rs({class => 'sipaccount'})->first; return unless $sipaccount; my $sipaccount_id = $sipaccount->id // 0; diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm index ed2b739d74..dc81f1f9b1 100644 --- a/lib/NGCP/Panel/Role/API.pm +++ b/lib/NGCP/Panel/Role/API.pm @@ -78,6 +78,7 @@ sub get_valid_data{ } return ($resource, $data); + } sub get_valid_post_data { @@ -414,7 +415,7 @@ sub allowed_methods_filtered { # #old: allowed_roles = [qw/admin subscriber /] # -#from now also possible: +#from now also possible: #sub config_allowed_roles { # return { # 'Default' => [qw/admin reseller subscriberadmin/], @@ -610,7 +611,6 @@ sub paginate_order_collection_rs { my ($self, $c, $item_rs, $params) = @_; my($page,$rows,$order_by,$direction) = @$params{qw/page rows order_by direction/}; - my $total_count; my $no_count = $self->dont_count_collection_total($c); if ( !$no_count ) { @@ -648,13 +648,13 @@ sub collection_nav_links { $params //= $c->request->params; delete @{$params}{'page', 'rows'}; - my $rest_params = join( '&', map {"$_=".$params->{$_}} keys %{$params}); + my $rest_params = join( '&', map {"$_=".(defined $params->{$_} ? $params->{$_} : '');} keys %{$params}); $rest_params = $rest_params ? "&$rest_params" : ""; my @links = (NGCP::Panel::Utils::DataHalLink->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s%s', $path, $page, $rows, $rest_params))); - if ( (! defined $total_count - && ! $c->stash->{collection_infinite_pager_stop} ) + if ( (! defined $total_count + && ! $c->stash->{collection_infinite_pager_stop} ) || ( defined $total_count && ($total_count / $rows) > $page ) ) { push @links, NGCP::Panel::Utils::DataHalLink->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d%s', $path, $page + 1, $rows, $rest_params)); } diff --git a/lib/NGCP/Panel/Role/API/Admins.pm b/lib/NGCP/Panel/Role/API/Admins.pm index db4071c29a..615a5a2f39 100644 --- a/lib/NGCP/Panel/Role/API/Admins.pm +++ b/lib/NGCP/Panel/Role/API/Admins.pm @@ -81,6 +81,7 @@ sub hal_from_item { $resource{id} = int($item->id); $hal->resource({%resource}); return $hal; + } sub item_by_id { diff --git a/lib/NGCP/Panel/Role/API/BalanceIntervals.pm b/lib/NGCP/Panel/Role/API/BalanceIntervals.pm index 3c26da5ecc..cc5f541982 100644 --- a/lib/NGCP/Panel/Role/API/BalanceIntervals.pm +++ b/lib/NGCP/Panel/Role/API/BalanceIntervals.pm @@ -63,8 +63,8 @@ sub hal_from_balance { my $contract = $item->contract; 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 $bm_start = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract, now => $item->start); + my $profile_at_start = $bm_start->billing_profile; my $is_actual = NGCP::Panel::Utils::DateTime::is_infinite_future($item->end) || NGCP::Panel::Utils::DateTime::set_local_tz($item->end) >= $now; my ($is_timely,$timely_start,$timely_end) = NGCP::Panel::Utils::ProfilePackages::get_timely_range( package => $contract->profile_package, diff --git a/lib/NGCP/Panel/Role/API/BillingProfiles.pm b/lib/NGCP/Panel/Role/API/BillingProfiles.pm index a193880b70..9c72e4ed72 100644 --- a/lib/NGCP/Panel/Role/API/BillingProfiles.pm +++ b/lib/NGCP/Panel/Role/API/BillingProfiles.pm @@ -166,7 +166,7 @@ sub update_profile { profile_id => $profile->id, old_prepaid => $old_prepaid, new_prepaid => $profile->prepaid, - contract_rs => NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB')), + contract_rs => NGCP::Panel::Utils::Contract::get_contract_rs(schema => $c->model('DB')), #ok ); } catch($e) { $c->log->error("Failed to update billing profile '".$profile->id."': $e"); diff --git a/lib/NGCP/Panel/Role/API/Capabilities.pm b/lib/NGCP/Panel/Role/API/Capabilities.pm index 1252302d17..df80bd37eb 100644 --- a/lib/NGCP/Panel/Role/API/Capabilities.pm +++ b/lib/NGCP/Panel/Role/API/Capabilities.pm @@ -85,7 +85,7 @@ sub _item_rs { }); my $customer = $customer_rs->first; - my $cpbx = ($customer->get_column('product_class') eq 'pbxaccount') ? 1 : 0; + my $cpbx = ($customer->product->class eq 'pbxaccount') ? 1 : 0; $cloudpbx &= $cpbx; # TODO: sms and rtcengine are not specially restricted; should it? diff --git a/lib/NGCP/Panel/Role/API/Contracts.pm b/lib/NGCP/Panel/Role/API/Contracts.pm index be38d8a7a7..89813fd1ba 100644 --- a/lib/NGCP/Panel/Role/API/Contracts.pm +++ b/lib/NGCP/Panel/Role/API/Contracts.pm @@ -13,6 +13,7 @@ use HTTP::Status qw(:constants); use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::Contract; use NGCP::Panel::Utils::ProfilePackages qw(); +use NGCP::Panel::Utils::BillingMappings qw(); sub _item_rs { my ($self, $c, $include_terminated,$now) = @_; @@ -23,12 +24,10 @@ sub _item_rs { now => $now, ); $item_rs = $item_rs->search({ - 'contact.reseller_id' => undef - },{ - join => 'contact', - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - }); + 'contact.reseller_id' => undef + },{ + join => 'contact', + }); return $item_rs; } @@ -41,10 +40,10 @@ sub get_form { sub hal_from_contract { my ($self, $c, $contract, $form, $now) = @_; - my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid')); + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, ); my $billing_profile_id = $billing_mapping->billing_profile_id; - my $future_billing_profiles = NGCP::Panel::Utils::Contract::resource_from_future_mappings($contract); - my $billing_profiles = NGCP::Panel::Utils::Contract::resource_from_mappings($contract); + my $future_billing_profiles = NGCP::Panel::Utils::BillingMappings::resource_from_future_mappings($contract); + my $billing_profiles = NGCP::Panel::Utils::BillingMappings::resource_from_mappings($contract); #we leave this here to keep the former behaviour: contract balances are also created upon GET api/contracts/4711 NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c, @@ -111,15 +110,17 @@ sub contract_by_id { sub update_contract { my ($self, $c, $contract, $old_resource, $resource, $form, $now) = @_; - my $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid')); + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, ); my $billing_profile = $billing_mapping->billing_profile; + $old_resource->{billing_mapping_id} = $billing_mapping->id; + my $old_package = $contract->profile_package; $form //= $self->get_form($c); # TODO: for some reason, formhandler lets missing contact_id slip thru $resource->{contact_id} //= undef; - $resource->{type} //= $billing_mapping->product->class; + $resource->{type} //= $contract->product->class; return unless $self->validate_form( c => $c, form => $form, @@ -132,7 +133,7 @@ sub update_contract { my $mappings_to_create = []; my $delete_mappings = 0; my $set_package = ($resource->{billing_profile_definition} // 'id') eq 'package'; - return unless NGCP::Panel::Utils::Contract::prepare_billing_mappings( + return unless NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $resource, old_resource => $old_resource, @@ -165,7 +166,7 @@ sub update_contract { try { $contract->update($resource); - NGCP::Panel::Utils::Contract::remove_future_billing_mappings($contract,$now) if $delete_mappings; + NGCP::Panel::Utils::BillingMappings::remove_future_billing_mappings($contract,$now) if $delete_mappings; foreach my $mapping (@$mappings_to_create) { $contract->billing_mappings->create($mapping); } @@ -183,7 +184,7 @@ sub update_contract { profiles_added => ($set_package ? scalar @$mappings_to_create : 0), ); - $billing_mapping = $contract->billing_mappings->find($contract->get_column('bmid')); + $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $contract, ); $billing_profile = $billing_mapping->billing_profile; if($old_resource->{status} ne $resource->{status}) { diff --git a/lib/NGCP/Panel/Role/API/Customers.pm b/lib/NGCP/Panel/Role/API/Customers.pm index eb1edeeea4..fbb7f71965 100644 --- a/lib/NGCP/Panel/Role/API/Customers.pm +++ b/lib/NGCP/Panel/Role/API/Customers.pm @@ -13,6 +13,7 @@ use HTTP::Status qw(:constants); use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::Contract; use NGCP::Panel::Utils::ProfilePackages qw(); +use NGCP::Panel::Utils::BillingMappings qw(); use NGCP::Panel::Utils::Preferences; use NGCP::Panel::Utils::Subscriber qw(); @@ -23,7 +24,7 @@ sub _item_rs { my $item_rs = NGCP::Panel::Utils::Contract::get_customer_rs( c => $c, include_terminated => 1, - now => $now, + #now => $now, ); return $item_rs; } @@ -41,10 +42,11 @@ sub hal_from_customer { $is_adm = 1; } - my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid')); + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, ); + #my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid')); my $billing_profile_id = $billing_mapping->billing_profile->id; - my $future_billing_profiles = NGCP::Panel::Utils::Contract::resource_from_future_mappings($customer); - my $billing_profiles = NGCP::Panel::Utils::Contract::resource_from_mappings($customer); + my $future_billing_profiles = NGCP::Panel::Utils::BillingMappings::resource_from_future_mappings($customer); + my $billing_profiles = NGCP::Panel::Utils::BillingMappings::resource_from_mappings($customer); NGCP::Panel::Utils::ProfilePackages::catchup_contract_balances(c => $c, contract => $customer, @@ -130,17 +132,19 @@ sub update_customer { return; } - my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid')); + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, ); + #my $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid')); my $billing_profile = $billing_mapping->billing_profile; my $old_package = $customer->profile_package; $old_resource->{prepaid} = $billing_profile->prepaid; + $old_resource->{billing_mapping_id} = $billing_mapping->id; $form //= $self->get_form($c); # TODO: for some reason, formhandler lets missing contact_id slip thru $resource->{contact_id} //= undef; - $resource->{type} //= $billing_mapping->product->class; + $resource->{type} //= $customer->product->class; return unless $self->validate_form( c => $c, form => $form, @@ -154,7 +158,7 @@ sub update_customer { my $mappings_to_create = []; my $delete_mappings = 0; my $set_package = ($resource->{billing_profile_definition} // 'id') eq 'package'; - return unless NGCP::Panel::Utils::Contract::prepare_billing_mappings( + return unless NGCP::Panel::Utils::BillingMappings::prepare_billing_mappings( c => $c, resource => $resource, old_resource => $old_resource, @@ -211,7 +215,7 @@ sub update_customer { try { $customer->update($resource); - NGCP::Panel::Utils::Contract::remove_future_billing_mappings($customer,$now) if $delete_mappings; + NGCP::Panel::Utils::BillingMappings::remove_future_billing_mappings($customer,$now) if $delete_mappings; foreach my $mapping (@$mappings_to_create) { $customer->billing_mappings->create($mapping); } @@ -229,7 +233,8 @@ sub update_customer { profiles_added => ($set_package ? scalar @$mappings_to_create : 0), ); - $billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid')); + $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, ); + #$billing_mapping = $customer->billing_mappings->find($customer->get_column('bmid')); $billing_profile = $billing_mapping->billing_profile; if(($customer->external_id // '') ne $old_ext_id) { diff --git a/lib/NGCP/Panel/Role/API/Subscribers.pm b/lib/NGCP/Panel/Role/API/Subscribers.pm index b23ee197fc..f91a100b5f 100644 --- a/lib/NGCP/Panel/Role/API/Subscribers.pm +++ b/lib/NGCP/Panel/Role/API/Subscribers.pm @@ -41,7 +41,7 @@ sub resource_from_item { my %resource = %{ merge($bill_resource, $prov_resource) }; $resource{administrative} = delete $resource{admin}; - unless($customer->get_column('product_class') eq 'pbxaccount') { + unless($customer->product->class eq 'pbxaccount') { delete $resource{is_pbx_group}; delete $resource{is_pbx_pilot}; delete $resource{pbx_extension}; @@ -70,7 +70,7 @@ sub resource_from_item { run => 0, ); - if($customer->get_column('product_class') eq 'pbxaccount') { + if($customer->product->class eq 'pbxaccount') { $resource{pbx_group_ids} = []; foreach my $group($item->provisioning_voip_subscriber->voip_pbx_groups->search_rs(undef,{'order_by' => 'me.id'})->all) { push @{ $resource{pbx_group_ids} }, int($group->group->voip_subscriber->id); @@ -261,10 +261,7 @@ sub get_customer { 'product.class' => 'sipaccount', 'product.class' => 'pbxaccount', ], - },{ - '+select' => [ 'billing_mappings.id', 'product.class' ], - '+as' => [ 'bmid', 'product_class' ], - }); + },undef); if($c->user->roles eq "admin") { } elsif($c->user->roles eq "reseller") { $customer_rs = $customer_rs->search({ @@ -280,11 +277,11 @@ sub get_customer { } sub get_billing_profile { - my ($self, $c, $customer) = @_; + my ($self, $c, $customer, $now) = @_; - my $mapping = $customer->billing_mappings->find($customer->get_column('bmid')); - if($mapping) { - return $mapping->billing_profile; + my $billing_mapping = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, now => $now, contract => $customer, ); + if($billing_mapping) { + return $billing_mapping->billing_profile; } else { $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'customer_id', doesn't have a valid billing mapping."); return; @@ -397,7 +394,7 @@ sub prepare_resource { } my $pilot; - if($customer->get_column('product_class') eq 'pbxaccount') { + if($customer->product->class eq 'pbxaccount') { $pilot = $customer->voip_subscribers->search({ 'provisioning_voip_subscriber.is_pbx_pilot' => 1, },{ @@ -419,7 +416,7 @@ sub prepare_resource { my $preferences = {}; my $admin = 0; - unless($customer->get_column('product_class') eq 'pbxaccount') { + unless($customer->product->class eq 'pbxaccount') { for my $pref(qw/is_pbx_group pbx_extension pbx_hunt_policy pbx_hunt_timeout is_pbx_pilot/) { delete $resource->{$pref}; } diff --git a/lib/NGCP/Panel/Utils/Billing.pm b/lib/NGCP/Panel/Utils/Billing.pm index 952c583690..2b10042cd3 100644 --- a/lib/NGCP/Panel/Utils/Billing.pm +++ b/lib/NGCP/Panel/Utils/Billing.pm @@ -444,6 +444,8 @@ sub get_datatable_cols { my ($c) = @_; return ( + { name => "prepaid", "search" => 0, "title" => $c->loc("Prepaid"), + custom_renderer => 'function ( data, type, full ) { data.escapeHtml = false; return \'\'; }' }, { name => "contract_cnt", "search" => 0, "title" => $c->loc("Used (contracts)"), }, { name => "package_cnt", "search" => 0, "title" => $c->loc("Used (packages)"), }, diff --git a/lib/NGCP/Panel/Utils/BillingMappings.pm b/lib/NGCP/Panel/Utils/BillingMappings.pm new file mode 100644 index 0000000000..ca56aa47e9 --- /dev/null +++ b/lib/NGCP/Panel/Utils/BillingMappings.pm @@ -0,0 +1,542 @@ +package NGCP::Panel::Utils::BillingMappings; +use strict; +use warnings; + +use Sipwise::Base; +use NGCP::Panel::Utils::DateTime qw(); +use DateTime::Format::Strptime qw(); + +sub get_actual_billing_mapping { + my %params = @_; + my ($c,$schema,$contract,$now) = @params{qw/c schema contract now/}; + $schema //= $c->model('DB'); + $now //= NGCP::Panel::Utils::DateTime::current_local; + my $contract_create = NGCP::Panel::Utils::DateTime::set_local_tz($contract->create_timestamp // $contract->modify_timestamp); + my $dtf = $schema->storage->datetime_parser; + $now = $contract_create if $now < $contract_create; #if there is no mapping starting with or before $now, it would returns the mapping with max(id): + my $bm_actual = $schema->resultset('billing_mappings_actual')->search({ contract_id => $contract->id },{bind => [ ( $dtf->format_datetime($now) ) x 2, ($contract->id) x 2 ],})->first; + if ($bm_actual) { + return $bm_actual->billing_mappings->first; + } + return undef; +} + +sub get_actual_billing_mapping_stmt { + my %params = @_; + my ($c,$schema,$contract,$now,$projection,$contract_id_alias) = @params{qw/c schema contract now projection contract_id_alias/}; + $schema //= $c->model('DB'); + $now //= NGCP::Panel::Utils::DateTime::current_local; + my $dtf = $schema->storage->datetime_parser; + $projection //= 'actual_billing_mapping.id'; + $contract_id_alias //= 'me.id'; + + return sprintf(<= %s or actual_billing_mapping.end_date is null) + and (actual_billing_mapping.start_date <= %s or actual_billing_mapping.start_date is null) + order by actual_billing_mapping.start_date desc, actual_billing_mapping.id desc limit 1 +EOS + , $projection, $contract_id_alias, ( map { '"'.$_.'"'; } ( $dtf->format_datetime($now), $dtf->format_datetime($now) ) )); + +} + +sub prepare_billing_mappings { + my (%params) = @_; + + my ($c,$resource,$old_resource,$mappings_to_create,$now,$delete_mappings,$err_code,$billing_profile_field,$billing_profiles_field,$profile_package_field,$billing_profile_definition_field) = @params{qw/c resource old_resource mappings_to_create now delete_mappings err_code billing_profile_field billing_profiles_field profile_package_field billing_profile_definition_field/}; + + my $schema = $c->model('DB'); + if (!defined $err_code || ref $err_code ne 'CODE') { + $err_code = sub { return 0; }; + } + + my $profile_def_mode = $resource->{billing_profile_definition} // 'id'; + $now //= NGCP::Panel::Utils::DateTime::current_local; + + my $reseller_id = undef; + my $is_customer = 1; + if (defined $resource->{contact_id}) { + my $contact = $schema->resultset('contacts')->find($resource->{contact_id}); + if ($contact) { + $reseller_id = $contact->reseller_id; #($contact->reseller_id // -1); + $is_customer = defined $reseller_id; + } + } + + my $product_id = undef; #any subsequent create will fail without product_id + my $prepaid = undef; + my $billing_profile_id = undef; + if (defined $old_resource) { + # TODO: what about changed product, do we allow it? + my $billing_mapping; + if (exists $old_resource->{billing_mapping_id}) { + $billing_mapping = $schema->resultset('billing_mappings')->find($old_resource->{billing_mapping_id}); + } elsif (exists $old_resource->{id}) { + $billing_mapping = get_actual_billing_mapping(schema => $schema, + contract => $schema->resultset('contracts')->find($old_resource->{id}), + now => $now); + #} else { + # return 0 unless &{$err_code}("No billing mapping or contract defined"); + } + $product_id = $billing_mapping->contract->product->id; + $prepaid = $billing_mapping->billing_profile->prepaid; + $billing_profile_id = $billing_mapping->billing_profile->id; + } else { + if (exists $resource->{type} || exists $c->stash->{type}) { + my $productclass = (exists $c->stash->{type} ? $c->stash->{type} : $resource->{type}); + my $product = $schema->resultset('products')->search_rs({ class => $productclass })->first; + if ($product) { + $product_id = $product->id; + } + } elsif (exists $resource->{product_id}) { + $product_id = $resource->{product_id}; + } + } + + if ('id' eq $profile_def_mode) { + my $delete = undef; + if (defined $old_resource) { #update + if (defined $resource->{billing_profile_id}) { + if ($billing_profile_id != $resource->{billing_profile_id}) { + #change profile: + $delete = 0; #1; #delete future mappings? + my $entities = {}; + return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, + resource => $resource, + profile_id_field => 'billing_profile_id', + field => $billing_profile_field, + ); + my ($profile) = @$entities{qw/profile/}; + push(@$mappings_to_create,{billing_profile_id => $profile->id, + network_id => undef, + product_id => $product_id, + start_date => $now, + end_date => undef, + }); + } else { + #not changed, don't touch mappings + $delete = 0; + } + } else { + #undef profile is not allowed + $delete = 0; + my $entities = {}; + return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, + resource => $resource, + profile_id_field => 'billing_profile_id', + field => $billing_profile_field, + ); + } + } else { #create + $delete = 1; #for the sake of completeness + my $entities = {}; + return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, + resource => $resource, + profile_id_field => 'billing_profile_id', + field => $billing_profile_field, + ); + my ($profile) = @$entities{qw/profile/}; + push(@$mappings_to_create,{billing_profile_id => $profile->id, + network_id => undef, + product_id => $product_id, + #we don't change the former behaviour in update situations: + start_date => undef, + end_date => undef, + }); + } + if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') { + $$delete_mappings = $delete; + } + delete $resource->{profile_package_id}; + } elsif ('profiles' eq $profile_def_mode) { + if (!defined $resource->{billing_profiles}) { + $resource->{billing_profiles} //= []; + } + if (ref $resource->{billing_profiles} ne "ARRAY") { + return 0 unless &{$err_code}("Invalid field 'billing_profiles'. Must be an array.",$billing_profiles_field); + } + my %interval_type_counts = ( open => 0, open_any_network => 0, 'open end' => 0, 'open start' => 0, 'start-end' => 0 ); + my $dtf = $schema->storage->datetime_parser; + foreach my $mapping (@{$resource->{billing_profiles}}) { + if (ref $mapping ne "HASH") { + return 0 unless &{$err_code}("Invalid element in array 'billing_profiles'. Must be an object.",$billing_profiles_field); + } + my $entities = {}; + return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, + resource => $mapping, + field => $billing_profiles_field, + profile_id_field => 'profile_id', + network_id_field => 'network_id', + ); + my ($profile,$network) = @$entities{qw/profile network/}; + if (defined $prepaid) { + if ($profile->prepaid != $prepaid) { + return 0 unless &{$err_code}("Future switching between prepaid and post-paid billing profiles is not supported (" . $profile->name . ").",$billing_profiles_field); + } + } else { + $prepaid = $profile->prepaid; + } + + # TODO: what about changed product, do we allow it? + #my $product_class = delete $mapping->{type}; + #unless( (defined $product_class ) && ($product_class eq "sipaccount" || $product_class eq "pbxaccount") ) { + # return 0 unless &{$err_code}("Mandatory 'type' parameter is empty or invalid, must be 'sipaccount' or 'pbxaccount'."); + #} + #my $product = $schema->resultset('products')->search_res({ class => $product_class })->first; + #unless($product) { + # return 0 unless &{$err_code}("Invalid 'type'."); + #} else { + # # add product_id just for form check (not part of the actual contract item) + # # and remove it after the check + # $mapping->{product_id} = $product->id; + #} + + my $start = (defined $mapping->{start} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{start}) : undef); + my $stop = (defined $mapping->{stop} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{stop}) : undef); + + if (!defined $start && !defined $stop) { #open interval + $interval_type_counts{open} += 1; + $interval_type_counts{open_any_network} += 1 unless $network; + } elsif (defined $start && !defined $stop) { #open end interval + my $start_str = $dtf->format_datetime($start); + if ($start <= $now) { + return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field); + } + #if (exists $start_dupes{$start_str}) { + # $start_dupes{$start_str} += 1; + # return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed."); + #} else { + # $start_dupes{$start_str} = 1; + #} + $interval_type_counts{'open end'} += 1; + } elsif (!defined $start && defined $stop) { #open start interval + my $stop_str = $dtf->format_datetime($stop); + return 0 unless &{$err_code}("Interval with 'stop' timestamp ($stop_str) but no 'start' timestamp specified.",$billing_profiles_field); + $interval_type_counts{'open start'} //= 0; + $interval_type_counts{'open start'} += 1; + } else { #start-end interval + my $start_str = $dtf->format_datetime($start); + if ($start <= $now) { + return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field); + } + my $stop_str = $dtf->format_datetime($stop); + if ($start >= $stop) { + return 0 unless &{$err_code}("'start' timestamp ($start_str) has to be before 'stop' timestamp ($stop_str).",$billing_profiles_field); + } + #if (exists $start_dupes{$start_str}) { + # $start_dupes{$start_str} += 1; + # return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed."); + #} else { + # $start_dupes{$start_str} = 1; + #} + $interval_type_counts{'start-end'} += 1; + } + + push(@$mappings_to_create,{ + billing_profile_id => $profile->id, + network_id => ($is_customer && defined $network ? $network->id : undef), + product_id => $product_id, + start_date => $start, + end_date => $stop, + }); + } + + if (!defined $old_resource && $interval_type_counts{'open_any_network'} < 1) { + return 0 unless &{$err_code}("An initial interval without 'start' and 'stop' timestamps and no billing network is required.",$billing_profiles_field); + } elsif (defined $old_resource && $interval_type_counts{'open'} > 0) { + return 0 unless &{$err_code}("Adding intervals without 'start' and 'stop' timestamps is not allowed.",$billing_profiles_field); + } + if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') { + $$delete_mappings = 1; #always clear future mappings to place new ones + } + delete $resource->{profile_package_id}; + } elsif ('package' eq $profile_def_mode) { + if (!$is_customer) { + return 0 unless &{$err_code}("Setting a profile package is supported for customer contracts only.",$billing_profile_definition_field); + } + my $delete = undef; + if (defined $old_resource) { #update + if (defined $old_resource->{profile_package_id} && !defined $resource->{profile_package_id}) { + #clear package: don't touch billing mappings (just clear profile package) + $delete = 0; + } elsif (!defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) { + #set package: apply initial mappings + $delete = 0; #1; #delete future mappings? + my $entities = {}; + return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, + package_id => $resource->{profile_package_id}, + field => $profile_package_field, + ); + my ($package) = @$entities{qw/package/}; + foreach my $mapping ($package->initial_profiles->all) { + push(@$mappings_to_create,{ #assume not terminated, + billing_profile_id => $mapping->profile_id, + network_id => ($is_customer ? $mapping->network_id : undef), + product_id => $product_id, + start_date => $now, + end_date => undef, + }); + } + } elsif (defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) { + if ($old_resource->{profile_package_id} != $resource->{profile_package_id}) { + #change package: apply initial mappings + $delete = 0; #1; #delete future mappings? + my $entities = {}; + return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, + package_id => $resource->{profile_package_id}, + field => $profile_package_field, + ); + my ($package) = @$entities{qw/package/}; + foreach my $mapping ($package->initial_profiles->all) { + push(@$mappings_to_create,{ #assume not terminated, + billing_profile_id => $mapping->profile_id, + network_id => ($is_customer ? $mapping->network_id : undef), + product_id => $product_id, + start_date => $now, + end_date => undef, + }); + } + } else { + #package unchanged: don't touch billing mappings + $delete = 0; + } + } else { + #package unchanged (null): don't touch billing mappings + $delete = 0; + } + } else { #create + $delete = 1; #for the sake of completeness + my $entities = {}; + return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, + package_id => $resource->{profile_package_id}, + field => $profile_package_field, + ); + my ($package) = @$entities{qw/package/}; + foreach my $mapping ($package->initial_profiles->all) { + push(@$mappings_to_create,{ #assume not terminated, + billing_profile_id => $mapping->profile_id, + network_id => ($is_customer ? $mapping->network_id : undef), + product_id => $product_id, + start_date => undef, #$now, + end_date => undef, + }); + } + } + if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') { + $$delete_mappings = $delete; + } + } else { + return 0 unless &{$err_code}("Invalid 'billing_profile_definition'.",$billing_profile_definition_field); + } + + delete $resource->{billing_profile_id}; + delete $resource->{billing_profiles}; + + delete $resource->{billing_profile_definition}; + + return 1; +} + +sub _check_profile_network { + my (%params) = @_; + my ($c,$res,$profile_id_field,$network_id_field,$field,$reseller_id,$err_code,$entities) = @params{qw/c resource profile_id_field network_id_field field reseller_id err_code entities/}; + + my $schema = $c->model('DB'); + if (!defined $err_code || ref $err_code ne 'CODE') { + $err_code = sub { return 0; }; + } + + unless(defined $res->{$profile_id_field}) { + return 0 unless &{$err_code}("Invalid '$profile_id_field', not defined.",$field); + } + my $profile = $schema->resultset('billing_profiles')->find($res->{$profile_id_field}); + unless($profile) { + return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}).",$field); + } + if ($profile->status eq 'terminated') { + return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}), already terminated.",$field); + } + if (defined $reseller_id && defined $profile->reseller_id && $reseller_id != $profile->reseller_id) { #($profile->reseller_id // -1)) { + return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing profile (" . $profile->name . ").",$field); + } + my $network; + if (defined $network_id_field && defined $res->{$network_id_field}) { + $network = $schema->resultset('billing_networks')->find($res->{$network_id_field}); + unless($network) { + return 0 unless &{$err_code}("Invalid '$network_id_field' ($res->{$network_id_field}).",$field); + } + if (defined $reseller_id && defined $network->reseller_id && $reseller_id != $network->reseller_id) { #($network->reseller_id // -1)) { + return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing network (" . $network->name . ").",$field); + } + } + if (defined $entities and ref $entities eq 'HASH') { + $entities->{profile} = $profile; + $entities->{network} = $network; + } + return 1; +} + +sub _check_profile_package { + my (%params) = @_; + my ($c,$res,$package_id,$reseller_id,$field,$err_code,$entities) = @params{qw/c resource package_id reseller_id field err_code entities/}; + + my $schema = $c->model('DB'); + if (!defined $err_code || ref $err_code ne 'CODE') { + $err_code = sub { return 0; }; + } + + unless(defined $package_id) { + return 0 unless &{$err_code}("Invalid 'profile_package_id', not defined.",$field); + } + my $package = $schema->resultset('profile_packages')->find($package_id); + unless($package) { + return 0 unless &{$err_code}("Invalid 'profile_package_id'.",$field); + } + + if (defined $reseller_id && defined $package->reseller_id && $reseller_id != $package->reseller_id) { + return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the profile package (" . $package->name . ").",$field); + } + + if (defined $entities and ref $entities eq 'HASH') { + $entities->{package} = $package; + } + return 1; +} + +sub resource_from_future_mappings { + my ($contract) = @_; + return resource_from_mappings($contract,1); +} + +sub resource_from_mappings { + + my ($contract,$future_only) = @_; + + my $is_customer = (defined $contract->contact->reseller_id ? 1 : 0); + my @mappings_resource = (); + + my $datetime_fmt = DateTime::Format::Strptime->new( + pattern => '%F %T', + ); #validate_forms uses RFC3339 otherwise, which contains the tz offset part + + foreach my $mapping (billing_mappings_ordered($future_only ? future_billing_mappings($contract->billing_mappings) : $contract->billing_mappings)->all) { + my %m = $mapping->get_inflated_columns; + delete $m{id}; + $m{start} = delete $m{start_date}; + $m{stop} = delete $m{end_date}; + $m{start} = $datetime_fmt->format_datetime($m{start}) if defined $m{start}; + $m{stop} = $datetime_fmt->format_datetime($m{stop}) if defined $m{stop}; + $m{profile_id} = delete $m{billing_profile_id}; + delete $m{contract_id}; + delete $m{product_id}; + delete $m{network_id} unless $is_customer; + push(@mappings_resource,\%m); + } + + return \@mappings_resource; + +} + +sub billing_mappings_ordered { + my ($rs,$now,$actual_bmid) = @_; + + my $dtf; + $dtf = $rs->result_source->schema->storage->datetime_parser if defined $now; + + my @select = (); + if ($now) { + push(@select,{ '' => \[ 'if(`me`.`start_date` is null,0,`me`.`start_date` > ?)', $dtf->format_datetime($now) ], -as => 'is_future' }); + } + if ($actual_bmid) { + push(@select,{ '' => \[ '`me`.`id` = ?', $actual_bmid ], -as => 'is_actual' }); + } + + return $rs->search_rs( + {}, + { order_by => { '-asc' => ['start_date', 'id']}, + (scalar @select == 1 ? ('+select' => $select[0]) : ()), + (scalar @select > 1 ? ('+select' => \@select) : ()), + }); + +} + +sub remove_future_billing_mappings { + + my ($contract,$now) = @_; + $now //= NGCP::Panel::Utils::DateTime::current_local; + + future_billing_mappings($contract->billing_mappings,$now)->delete; + +} + +sub future_billing_mappings { + + my ($rs,$now) = @_; + $now //= NGCP::Panel::Utils::DateTime::current_local; + + return $rs->search_rs({start_date => { '>' => $now },}); + +} + +sub get_billingmappings_timeline_data { + my ($c,$contract,$range) = @_; + unless ($range) { + $range = eval { $c->req->body_data; }; + if ($@) { + $c->log->error('error decoding timeline json request: ' . $@); + } + } + my $start; + $start = NGCP::Panel::Utils::DateTime::from_string($range->{start}) if $range->{start}; + my $end; + $end = NGCP::Panel::Utils::DateTime::from_string($range->{end}) if $range->{end}; + $c->log->debug("timeline range $start - $end"); + #the max start date (of mappings with NULL end date) less than + #the visible range end will become the range start: + my $max_start_date = $contract->billing_mappings->search({ + ($end ? (start_date => [ -or => + { '<=' => $end }, + { '=' => undef }, + ]) : ()), + end_date => { '=' => undef }, + },{ + order_by => { '-desc' => ['start_date', 'me.id']}, #NULL start dates at last + })->first; + #lower the range start, if required: + if ($max_start_date) { + if ($max_start_date->start_date) { + $start = $max_start_date->start_date if (not $start or $max_start_date->start_date < $start); + } else { + $start = $max_start_date->start_date; + } + } + my $res = $contract->billing_mappings->search({ + ($end ? (start_date => ($start ? [ -and => { + '<=' => $end },{ #hide mappings beginning after range end + '>=' => $start #and beginning before range start (max_start_date). + },] : [ -or => { #if there is a mapping with NULL start only, + '<=' => $end },{ #include all mapping beginning before range end. + '=' => undef + },])) : ()), + },{ + order_by => { '-asc' => ['start_date', 'me.id']}, + prefetch => [ 'billing_profile' , 'network' ] + }); + my @timeline_events = map { + { $_->get_columns, + billing_profile => { ($_->billing_profile ? ( name => $_->billing_profile->name, ) : ()) }, + network => { ($_->network ? ( name => $_->network->name, ) : ()) }, + }; + } $res->all; + return \@timeline_events; +} + +1; diff --git a/lib/NGCP/Panel/Utils/Contract.pm b/lib/NGCP/Panel/Utils/Contract.pm index 6fa670e8f5..dcb65ae2fa 100644 --- a/lib/NGCP/Panel/Utils/Contract.pm +++ b/lib/NGCP/Panel/Utils/Contract.pm @@ -5,7 +5,7 @@ use warnings; use Sipwise::Base; use DBIx::Class::Exception; use NGCP::Panel::Utils::DateTime; -use DateTime::Format::Strptime qw(); +#use DateTime::Format::Strptime qw(); use NGCP::Panel::Utils::CallList qw(); sub recursively_lock_contract { @@ -108,71 +108,48 @@ sub recursively_lock_contract { } sub get_contract_rs { - my %params = @_; - my ($schema,$now,$contract_id) = @params{qw/schema now contract_id/}; - $now //= NGCP::Panel::Utils::DateTime::current_local; - my $dtf = $schema->storage->datetime_parser; - my $rs = $schema->resultset('contracts') - ->search({ - $params{include_terminated} ? () : ('me.status' => { '!=' => 'terminated' }), - },{ - bind => [ ( $dtf->format_datetime($now) ) x 2, ( $contract_id ) x 2 ], - 'join' => { 'billing_mappings_actual' => { 'billing_mappings' => 'product'}}, - '+select' => [ - 'billing_mappings.id', - 'billing_mappings.start_date', - 'billing_mappings.product_id', - ], - '+as' => [ - 'billing_mapping_id', - 'billing_mapping_start_date', - 'product_id', - ], - alias => 'me', - }); + my %params = @_; + my ($c,$schema,$include_terminated) = @params{qw/c schema include_terminated/}; + $schema //= $c->model('DB'); + my $rs = $schema->resultset('contracts')->search({ + $include_terminated ? () : ('me.status' => { '!=' => 'terminated' }), + },{ + join => 'product', + }); return $rs; + } sub get_customer_rs { my %params = @_; - my ($c,$now,$contract_id) = @params{qw/c now contract_id/}; - - my $customers = get_contract_rs( - schema => $c->model('DB'), - include_terminated => $params{include_terminated}, - now => $now, - contract_id => $contract_id, - ); - - $customers = $customers->search({ - 'contact.reseller_id' => { '-not' => undef }, + my ($c,$schema,$include_terminated) = @params{qw/c schema include_terminated/}; + + my $rs = get_contract_rs( + c => $c, + schema => $schema, + include_terminated => $include_terminated, + )->search_rs({ + 'product.class' => { -in => [ 'sipaccount', 'pbxaccount' ] }, },{ join => 'contact', }); if($c->user->roles eq "admin") { + $rs = $rs->search_rs({ + 'contact.reseller_id' => { '-not' => undef }, + },undef); } elsif($c->user->roles eq "reseller") { - $customers = $customers->search({ - 'contact.reseller_id' => $c->user->reseller_id, - }); - } elsif($c->user->roles eq "subscriberadmin") { - $customers = $customers->search({ - 'contact.reseller_id' => $c->user->contract->contact->reseller_id, - }); + $rs = $rs->search({ + 'contact.reseller_id' => $c->user->reseller_id, + },undef); + } elsif($c->user->roles eq "subscriberadmin" or $c->user->roles eq "subscriber") { + $rs = $rs->search({ + 'contact.reseller_id' => $c->user->contract->contact->reseller_id, + },undef); } - $customers = $customers->search({ - '-or' => [ - 'product.class' => 'sipaccount', - 'product.class' => 'pbxaccount', - ], - },{ - '+select' => 'billing_mappings.id', - '+as' => 'bmid', - }); - - return $customers; + return $rs; } sub get_contract_zonesfees_rs { @@ -327,488 +304,6 @@ sub get_contract_calls_rs{ } -sub prepare_billing_mappings { - my (%params) = @_; - - my ($c,$resource,$old_resource,$mappings_to_create,$now,$delete_mappings,$err_code,$billing_profile_field,$billing_profiles_field,$profile_package_field,$billing_profile_definition_field) = @params{qw/c resource old_resource mappings_to_create now delete_mappings err_code billing_profile_field billing_profiles_field profile_package_field billing_profile_definition_field/}; - - my $schema = $c->model('DB'); - if (!defined $err_code || ref $err_code ne 'CODE') { - $err_code = sub { return 0; }; - } - - my $profile_def_mode = $resource->{billing_profile_definition} // 'id'; - $now //= NGCP::Panel::Utils::DateTime::current_local; - - my $reseller_id = undef; - my $is_customer = 1; - if (defined $resource->{contact_id}) { - my $contact = $schema->resultset('contacts')->find($resource->{contact_id}); - if ($contact) { - $reseller_id = $contact->reseller_id; #($contact->reseller_id // -1); - $is_customer = defined $reseller_id; - } - } - - my $product_id = undef; #any subsequent create will fail without product_id - my $prepaid = undef; - my $billing_profile_id = undef; - if (defined $old_resource) { - # TODO: what about changed product, do we allow it? - my $billing_mapping = $schema->resultset('billing_mappings')->find($old_resource->{billing_mapping_id}); - $product_id = $billing_mapping->product->id; - $prepaid = $billing_mapping->billing_profile->prepaid; - $billing_profile_id = $billing_mapping->billing_profile->id; - } else { - if (exists $resource->{type} || exists $c->stash->{type}) { - my $productclass = (exists $c->stash->{type} ? $c->stash->{type} : $resource->{type}); - my $product = $schema->resultset('products')->find({ class => $productclass }); - if ($product) { - $product_id = $product->id; - } - } elsif (exists $resource->{product_id}) { - $product_id = $resource->{product_id}; - } - } - - if ('id' eq $profile_def_mode) { - my $delete = undef; - if (defined $old_resource) { #update - if (defined $resource->{billing_profile_id}) { - if ($billing_profile_id != $resource->{billing_profile_id}) { - #change profile: - $delete = 0; #1; #delete future mappings? - my $entities = {}; - return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, - resource => $resource, - profile_id_field => 'billing_profile_id', - field => $billing_profile_field, - ); - my ($profile) = @$entities{qw/profile/}; - push(@$mappings_to_create,{billing_profile_id => $profile->id, - network_id => undef, - product_id => $product_id, - start_date => $now, - end_date => undef, - }); - } else { - #not changed, don't touch mappings - $delete = 0; - } - } else { - #undef profile is not allowed - $delete = 0; - my $entities = {}; - return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, - resource => $resource, - profile_id_field => 'billing_profile_id', - field => $billing_profile_field, - ); - } - } else { #create - $delete = 1; #for the sake of completeness - my $entities = {}; - return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, - resource => $resource, - profile_id_field => 'billing_profile_id', - field => $billing_profile_field, - ); - my ($profile) = @$entities{qw/profile/}; - push(@$mappings_to_create,{billing_profile_id => $profile->id, - network_id => undef, - product_id => $product_id, - #we don't change the former behaviour in update situations: - start_date => undef, - end_date => undef, - }); - } - if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') { - $$delete_mappings = $delete; - } - delete $resource->{profile_package_id}; - } elsif ('profiles' eq $profile_def_mode) { - if (!defined $resource->{billing_profiles}) { - $resource->{billing_profiles} //= []; - } - if (ref $resource->{billing_profiles} ne "ARRAY") { - return 0 unless &{$err_code}("Invalid field 'billing_profiles'. Must be an array.",$billing_profiles_field); - } - my %interval_type_counts = ( open => 0, open_any_network => 0, 'open end' => 0, 'open start' => 0, 'start-end' => 0 ); - my $dtf = $schema->storage->datetime_parser; - foreach my $mapping (@{$resource->{billing_profiles}}) { - if (ref $mapping ne "HASH") { - return 0 unless &{$err_code}("Invalid element in array 'billing_profiles'. Must be an object.",$billing_profiles_field); - } - my $entities = {}; - return 0 unless _check_profile_network(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, - resource => $mapping, - field => $billing_profiles_field, - profile_id_field => 'profile_id', - network_id_field => 'network_id', - ); - my ($profile,$network) = @$entities{qw/profile network/}; - if (defined $prepaid) { - if ($profile->prepaid != $prepaid) { - return 0 unless &{$err_code}("Future switching between prepaid and post-paid billing profiles is not supported (" . $profile->name . ").",$billing_profiles_field); - } - } else { - $prepaid = $profile->prepaid; - } - - # TODO: what about changed product, do we allow it? - #my $product_class = delete $mapping->{type}; - #unless( (defined $product_class ) && ($product_class eq "sipaccount" || $product_class eq "pbxaccount") ) { - # return 0 unless &{$err_code}("Mandatory 'type' parameter is empty or invalid, must be 'sipaccount' or 'pbxaccount'."); - #} - #my $product = $schema->resultset('products')->find({ class => $product_class }); - #unless($product) { - # return 0 unless &{$err_code}("Invalid 'type'."); - #} else { - # # add product_id just for form check (not part of the actual contract item) - # # and remove it after the check - # $mapping->{product_id} = $product->id; - #} - - my $start = (defined $mapping->{start} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{start}) : undef); - my $stop = (defined $mapping->{stop} ? NGCP::Panel::Utils::DateTime::from_string($mapping->{stop}) : undef); - - if (!defined $start && !defined $stop) { #open interval - $interval_type_counts{open} += 1; - $interval_type_counts{open_any_network} += 1 unless $network; - } elsif (defined $start && !defined $stop) { #open end interval - my $start_str = $dtf->format_datetime($start); - if ($start <= $now) { - return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field); - } - #if (exists $start_dupes{$start_str}) { - # $start_dupes{$start_str} += 1; - # return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed."); - #} else { - # $start_dupes{$start_str} = 1; - #} - $interval_type_counts{'open end'} += 1; - } elsif (!defined $start && defined $stop) { #open start interval - my $stop_str = $dtf->format_datetime($stop); - return 0 unless &{$err_code}("Interval with 'stop' timestamp ($stop_str) but no 'start' timestamp specified.",$billing_profiles_field); - $interval_type_counts{'open start'} //= 0; - $interval_type_counts{'open start'} += 1; - } else { #start-end interval - my $start_str = $dtf->format_datetime($start); - if ($start <= $now) { - return 0 unless &{$err_code}("'start' timestamp ($start_str) is not in future.",$billing_profiles_field); - } - my $stop_str = $dtf->format_datetime($stop); - if ($start >= $stop) { - return 0 unless &{$err_code}("'start' timestamp ($start_str) has to be before 'stop' timestamp ($stop_str).",$billing_profiles_field); - } - #if (exists $start_dupes{$start_str}) { - # $start_dupes{$start_str} += 1; - # return 0 unless &{$err_code}("Identical 'start' timestamps ($start_str) not allowed."); - #} else { - # $start_dupes{$start_str} = 1; - #} - $interval_type_counts{'start-end'} += 1; - } - - push(@$mappings_to_create,{ - billing_profile_id => $profile->id, - network_id => ($is_customer && defined $network ? $network->id : undef), - product_id => $product_id, - start_date => $start, - end_date => $stop, - }); - } - - if (!defined $old_resource && $interval_type_counts{'open_any_network'} < 1) { - return 0 unless &{$err_code}("An initial interval without 'start' and 'stop' timestamps and no billing network is required.",$billing_profiles_field); - } elsif (defined $old_resource && $interval_type_counts{'open'} > 0) { - return 0 unless &{$err_code}("Adding intervals without 'start' and 'stop' timestamps is not allowed.",$billing_profiles_field); - } - if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') { - $$delete_mappings = 1; #always clear future mappings to place new ones - } - delete $resource->{profile_package_id}; - } elsif ('package' eq $profile_def_mode) { - if (!$is_customer) { - return 0 unless &{$err_code}("Setting a profile package is supported for customer contracts only.",$billing_profile_definition_field); - } - my $delete = undef; - if (defined $old_resource) { #update - if (defined $old_resource->{profile_package_id} && !defined $resource->{profile_package_id}) { - #clear package: don't touch billing mappings (just clear profile package) - $delete = 0; - } elsif (!defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) { - #set package: apply initial mappings - $delete = 0; #1; #delete future mappings? - my $entities = {}; - return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, - package_id => $resource->{profile_package_id}, - field => $profile_package_field, - ); - my ($package) = @$entities{qw/package/}; - foreach my $mapping ($package->initial_profiles->all) { - push(@$mappings_to_create,{ #assume not terminated, - billing_profile_id => $mapping->profile_id, - network_id => ($is_customer ? $mapping->network_id : undef), - product_id => $product_id, - start_date => $now, - end_date => undef, - }); - } - } elsif (defined $old_resource->{profile_package_id} && defined $resource->{profile_package_id}) { - if ($old_resource->{profile_package_id} != $resource->{profile_package_id}) { - #change package: apply initial mappings - $delete = 0; #1; #delete future mappings? - my $entities = {}; - return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, - package_id => $resource->{profile_package_id}, - field => $profile_package_field, - ); - my ($package) = @$entities{qw/package/}; - foreach my $mapping ($package->initial_profiles->all) { - push(@$mappings_to_create,{ #assume not terminated, - billing_profile_id => $mapping->profile_id, - network_id => ($is_customer ? $mapping->network_id : undef), - product_id => $product_id, - start_date => $now, - end_date => undef, - }); - } - } else { - #package unchanged: don't touch billing mappings - $delete = 0; - } - } else { - #package unchanged (null): don't touch billing mappings - $delete = 0; - } - } else { #create - $delete = 1; #for the sake of completeness - my $entities = {}; - return 0 unless _check_profile_package(c => $c, reseller_id => $reseller_id, err_code => $err_code, entities => $entities, - package_id => $resource->{profile_package_id}, - field => $profile_package_field, - ); - my ($package) = @$entities{qw/package/}; - foreach my $mapping ($package->initial_profiles->all) { - push(@$mappings_to_create,{ #assume not terminated, - billing_profile_id => $mapping->profile_id, - network_id => ($is_customer ? $mapping->network_id : undef), - product_id => $product_id, - start_date => undef, #$now, - end_date => undef, - }); - } - } - if (defined $delete_mappings && ref $delete_mappings eq 'SCALAR') { - $$delete_mappings = $delete; - } - } else { - return 0 unless &{$err_code}("Invalid 'billing_profile_definition'.",$billing_profile_definition_field); - } - - delete $resource->{billing_profile_id}; - delete $resource->{billing_profiles}; - - delete $resource->{billing_profile_definition}; - - return 1; -} - -sub _check_profile_network { - my (%params) = @_; - my ($c,$res,$profile_id_field,$network_id_field,$field,$reseller_id,$err_code,$entities) = @params{qw/c resource profile_id_field network_id_field field reseller_id err_code entities/}; - - my $schema = $c->model('DB'); - if (!defined $err_code || ref $err_code ne 'CODE') { - $err_code = sub { return 0; }; - } - - unless(defined $res->{$profile_id_field}) { - return 0 unless &{$err_code}("Invalid '$profile_id_field', not defined.",$field); - } - my $profile = $schema->resultset('billing_profiles')->find($res->{$profile_id_field}); - unless($profile) { - return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}).",$field); - } - if ($profile->status eq 'terminated') { - return 0 unless &{$err_code}("Invalid '$profile_id_field' ($res->{$profile_id_field}), already terminated.",$field); - } - if (defined $reseller_id && defined $profile->reseller_id && $reseller_id != $profile->reseller_id) { #($profile->reseller_id // -1)) { - return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing profile (" . $profile->name . ").",$field); - } - my $network; - if (defined $network_id_field && defined $res->{$network_id_field}) { - $network = $schema->resultset('billing_networks')->find($res->{$network_id_field}); - unless($network) { - return 0 unless &{$err_code}("Invalid '$network_id_field' ($res->{$network_id_field}).",$field); - } - if (defined $reseller_id && defined $network->reseller_id && $reseller_id != $network->reseller_id) { #($network->reseller_id // -1)) { - return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the billing network (" . $network->name . ").",$field); - } - } - if (defined $entities and ref $entities eq 'HASH') { - $entities->{profile} = $profile; - $entities->{network} = $network; - } - return 1; -} - -sub _check_profile_package { - my (%params) = @_; - my ($c,$res,$package_id,$reseller_id,$field,$err_code,$entities) = @params{qw/c resource package_id reseller_id field err_code entities/}; - - my $schema = $c->model('DB'); - if (!defined $err_code || ref $err_code ne 'CODE') { - $err_code = sub { return 0; }; - } - - unless(defined $package_id) { - return 0 unless &{$err_code}("Invalid 'profile_package_id', not defined.",$field); - } - my $package = $schema->resultset('profile_packages')->find($package_id); - unless($package) { - return 0 unless &{$err_code}("Invalid 'profile_package_id'.",$field); - } - - if (defined $reseller_id && defined $package->reseller_id && $reseller_id != $package->reseller_id) { - return 0 unless &{$err_code}("The reseller of the contact doesn't match the reseller of the profile package (" . $package->name . ").",$field); - } - - if (defined $entities and ref $entities eq 'HASH') { - $entities->{package} = $package; - } - return 1; -} - -sub resource_from_future_mappings { - my ($contract) = @_; - return resource_from_mappings($contract,1); -} - -sub resource_from_mappings { - - my ($contract,$future_only) = @_; - - my $is_customer = (defined $contract->contact->reseller_id ? 1 : 0); - my @mappings_resource = (); - - my $datetime_fmt = DateTime::Format::Strptime->new( - pattern => '%F %T', - ); #validate_forms uses RFC3339 otherwise, which contains the tz offset part - - foreach my $mapping (billing_mappings_ordered($future_only ? future_billing_mappings($contract->billing_mappings) : $contract->billing_mappings)->all) { - my %m = $mapping->get_inflated_columns; - delete $m{id}; - $m{start} = delete $m{start_date}; - $m{stop} = delete $m{end_date}; - $m{start} = $datetime_fmt->format_datetime($m{start}) if defined $m{start}; - $m{stop} = $datetime_fmt->format_datetime($m{stop}) if defined $m{stop}; - $m{profile_id} = delete $m{billing_profile_id}; - delete $m{contract_id}; - delete $m{product_id}; - delete $m{network_id} unless $is_customer; - push(@mappings_resource,\%m); - } - - return \@mappings_resource; - -} - -sub billing_mappings_ordered { - my ($rs,$now,$actual_bmid) = @_; - - my $dtf; - $dtf = $rs->result_source->schema->storage->datetime_parser if defined $now; - - my @select = (); - if ($now) { - push(@select,{ '' => \[ 'if(`me`.`start_date` is null,0,`me`.`start_date` > ?)', $dtf->format_datetime($now) ], -as => 'is_future' }); - } - if ($actual_bmid) { - push(@select,{ '' => \[ '`me`.`id` = ?', $actual_bmid ], -as => 'is_actual' }); - } - - return $rs->search_rs( - {}, - { order_by => { '-asc' => ['start_date', 'id']}, - (scalar @select == 1 ? ('+select' => $select[0]) : ()), - (scalar @select > 1 ? ('+select' => \@select) : ()), - }); - -} - -sub remove_future_billing_mappings { - - my ($contract,$now) = @_; - $now //= NGCP::Panel::Utils::DateTime::current_local; - - future_billing_mappings($contract->billing_mappings,$now)->delete; - -} - -sub future_billing_mappings { - - my ($rs,$now) = @_; - $now //= NGCP::Panel::Utils::DateTime::current_local; - - return $rs->search_rs({start_date => { '>' => $now },}); - -} - -sub get_billingmappings_timeline_data { - my ($c,$contract,$range) = @_; - unless ($range) { - $range = eval { $c->req->body_data; }; - if ($@) { - $c->log->error('error decoding timeline json request: ' . $@); - } - } - my $start; - $start = NGCP::Panel::Utils::DateTime::from_string($range->{start}) if $range->{start}; - my $end; - $end = NGCP::Panel::Utils::DateTime::from_string($range->{end}) if $range->{end}; - $c->log->debug("timeline range $start - $end"); - #the max start date (of mappings with NULL end date) less than - #the visible range end will become the range start: - my $max_start_date = $contract->billing_mappings->search({ - ($end ? (start_date => [ -or => - { '<=' => $end }, - { '=' => undef }, - ]) : ()), - end_date => { '=' => undef }, - },{ - order_by => { '-desc' => ['start_date', 'me.id']}, #NULL start dates at last - })->first; - #lower the range start, if required: - if ($max_start_date) { - if ($max_start_date->start_date) { - $start = $max_start_date->start_date if (not $start or $max_start_date->start_date < $start); - } else { - $start = $max_start_date->start_date; - } - } - my $res = $contract->billing_mappings->search({ - ($end ? (start_date => ($start ? [ -and => { - '<=' => $end },{ #hide mappings beginning after range end - '>=' => $start #and beginning before range start (max_start_date). - },] : [ -or => { #if there is a mapping with NULL start only, - '<=' => $end },{ #include all mapping beginning before range end. - '=' => undef - },])) : ()), - },{ - order_by => { '-asc' => ['start_date', 'me.id']}, - prefetch => [ 'billing_profile' , 'network' ] - }); - my @timeline_events = map { - { $_->get_columns, - billing_profile => { ($_->billing_profile ? ( name => $_->billing_profile->name, ) : ()) }, - network => { ($_->network ? ( name => $_->network->name, ) : ()) }, - }; - } $res->all; - return \@timeline_events; -} - 1; __END__ diff --git a/lib/NGCP/Panel/Utils/Invoice.pm b/lib/NGCP/Panel/Utils/Invoice.pm index 663444b7c6..51cb126e58 100644 --- a/lib/NGCP/Panel/Utils/Invoice.pm +++ b/lib/NGCP/Panel/Utils/Invoice.pm @@ -1,7 +1,8 @@ package NGCP::Panel::Utils::Invoice; use Sipwise::Base; -use NGCP::Panel::Utils::ProfilePackages; +use NGCP::Panel::Utils::ProfilePackages qw(); +use NGCP::Panel::Utils::BillingMappings qw(); use NGCP::Panel::Utils::InvoiceTemplate; use NGCP::Panel::Utils::Contract; use NGCP::Panel::Utils::Message; @@ -18,12 +19,12 @@ sub get_invoice_amounts{ $billing_profile->{interval_charge} //= 0; $customer_contract->{vat_rate} //= 0; #use Data::Dumper; - #print Dumper [$contract_balance,$billing_profile]; + #print Dumper [$contract_balance,$billing_profile]; $invoice->{amount_net} = $contract_balance->{cash_balance_interval} / 100 + $billing_profile->{interval_charge}; - $invoice->{amount_vat} = - $customer_contract->{add_vat} + $invoice->{amount_vat} = + $customer_contract->{add_vat} ? - $invoice->{amount_net} * ($customer_contract->{vat_rate}/100) + $invoice->{amount_net} * ($customer_contract->{vat_rate}/100) : 0, $invoice->{amount_total} = $invoice->{amount_net} + $invoice->{amount_vat}; return $invoice; @@ -60,11 +61,11 @@ sub create_invoice{ etime => $etime,); $stime = $balance->start; $etime = $balance->end; - my $bm_actual = NGCP::Panel::Utils::ProfilePackages::get_actual_billing_mapping( - c => $c, - contract => $customer, + my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping( + c => $c, + contract => $customer, now => $balance->start); - my $billing_profile = $bm_actual->billing_mappings->first->billing_profile; + my $billing_profile = $bm_actual->billing_profile; my $zonecalls = NGCP::Panel::Utils::Contract::get_contract_zonesfees( c => $c, contract_id => $contract_id, @@ -124,9 +125,9 @@ sub create_invoice{ try { $invoice = $schema->resultset('invoices')->create($invoice_data); } catch($e) { - die { - showdetails => $c->loc('Failed to save invoice meta data.'), - error => $e, + die { + showdetails => $c->loc('Failed to save invoice meta data.'), + error => $e, httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } @@ -174,9 +175,9 @@ sub create_invoice{ my $error = $t->error(); my $error_msg = "error processing template, type=".$error->type.", info='".$error->info."'"; my $msg =$c->loc('Failed to render template. Type is [_1], info is [_2].', $error->type, $error->info); - die { - showdetails => $msg, - error => $error_msg, + die { + showdetails => $msg, + error => $error_msg, httpcode => HTTP_UNPROCESSABLE_ENTITY, }; }; @@ -205,12 +206,11 @@ sub check_invoice_data{ my $invoice_data = {}; my $schema = $c->model('DB'); - my $customer_rs = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c, contract_id => $contract_id); - my $customer = $customer_rs->find({ 'me.id' => $contract_id }); + my $customer = NGCP::Panel::Utils::Contract::get_customer_rs(c => $c)->find({ 'me.id' => $contract_id }); unless($customer) { - die { - showdetails => $c->loc('Customer not found'), - error => "invalid contract_id $contract_id", + die { + showdetails => $c->loc('Customer not found'), + error => "invalid contract_id $contract_id", httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } @@ -229,24 +229,24 @@ sub check_invoice_data{ $tmpl = $tmpl->first; unless($tmpl) { - die { - showdetails => $c->loc('Invoice template not found'), - error => "invalid template id $tmpl_id", + die { + showdetails => $c->loc('Invoice template not found'), + error => "invalid template id $tmpl_id", httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } unless($tmpl->data) { - die { - showdetails => $c->loc('Invoice template does not have an SVG stored yet'), - error => "invalid template id $tmpl_id, data is empty", + die { + showdetails => $c->loc('Invoice template does not have an SVG stored yet'), + error => "invalid template id $tmpl_id, data is empty", httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } unless($customer->contact->reseller_id == $tmpl->reseller_id) { - die { - showdetails => $c->loc('Template and customer must belong to same reseller'), - error => "template id ".$tmpl->id." has different reseller than contract id $contract_id", + die { + showdetails => $c->loc('Template and customer must belong to same reseller'), + error => "template id ".$tmpl->id." has different reseller than contract id $contract_id", httpcode => HTTP_UNPROCESSABLE_ENTITY, }; } diff --git a/lib/NGCP/Panel/Utils/ProfilePackages.pm b/lib/NGCP/Panel/Utils/ProfilePackages.pm index 45659b5a27..99d098e41d 100644 --- a/lib/NGCP/Panel/Utils/ProfilePackages.pm +++ b/lib/NGCP/Panel/Utils/ProfilePackages.pm @@ -6,6 +6,7 @@ use Scalar::Util qw(looks_like_number); #use TryCatch; use NGCP::Panel::Utils::DateTime qw(); use NGCP::Panel::Utils::Subscriber qw(); +use NGCP::Panel::Utils::BillingMappings qw(); use Data::Dumper; use constant INITIAL_PROFILE_DISCRIMINATOR => 'initial'; @@ -261,13 +262,13 @@ sub catchup_contract_balances { my $bm_actual; unless ($last_profile) { - $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => NGCP::Panel::Utils::DateTime::set_local_tz($last_balance->start)); - $last_profile = $bm_actual->billing_mappings->first->billing_profile; + $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => NGCP::Panel::Utils::DateTime::set_local_tz($last_balance->start)); + $last_profile = $bm_actual->billing_profile; } my ($underrun_profiles_ts,$underrun_lock_ts) = (undef,undef); PREPARE_BALANCE_CATCHUP: - $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $start_of_next_interval); - my $profile = $bm_actual->billing_mappings->first->billing_profile; + $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $start_of_next_interval); + my $profile = $bm_actual->billing_profile; $interval_unit = $has_package ? $interval_unit : ($profile->interval_unit // _DEFAULT_PROFILE_INTERVAL_UNIT); $interval_value = $has_package ? $interval_value : ($profile->interval_count // _DEFAULT_PROFILE_INTERVAL_COUNT); @@ -402,8 +403,8 @@ sub set_contract_balance { $log_vals->{old_package} = ( $package ? { $package->get_inflated_columns } : undef); $log_vals->{new_package} = $log_vals->{old_package}; $log_vals->{old_balance} = { $balance->get_inflated_columns }; - my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); - my $profile = $bm_actual->billing_mappings->first->billing_profile; + my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); + my $profile = $bm_actual->billing_profile; $log_vals->{old_profile} = { $profile->get_inflated_columns }; $log_vals->{amount} = $cash_balance - $balance->cash_balance; } @@ -420,8 +421,8 @@ sub set_contract_balance { $contract->discard_changes(); if ($log_vals) { $log_vals->{new_balance} = { $balance->get_inflated_columns }; - my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); - my $profile = $bm_actual->billing_mappings->first->billing_profile; + my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); + my $profile = $bm_actual->billing_profile; $log_vals->{new_profile} = { $profile->get_inflated_columns }; } @@ -455,8 +456,8 @@ sub topup_contract_balance { now => $now); if ($log_vals) { $log_vals->{old_balance} = { $balance->get_inflated_columns }; - my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); - my $profile = $bm_actual->billing_mappings->first->billing_profile; + my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); + my $profile = $bm_actual->billing_profile; $log_vals->{old_profile} = { $profile->get_inflated_columns }; if ($subscriber) { $log_vals->{old_lock_level} = NGCP::Panel::Utils::Subscriber::get_provisoning_voip_subscriber_lock_level( @@ -513,8 +514,8 @@ sub topup_contract_balance { $contract->discard_changes(); if ($log_vals) { $log_vals->{new_balance} = { $balance->get_inflated_columns }; - my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); - my $profile = $bm_actual->billing_mappings->first->billing_profile; + my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); + my $profile = $bm_actual->billing_profile; $log_vals->{new_profile} = { $profile->get_inflated_columns }; } @@ -591,8 +592,8 @@ sub create_initial_contract_balances { my ($underrun_lock_ts,$underrun_profiles_ts) = (undef,undef); my ($underrun_lock_applied,$underrun_profiles_applied) = (0,0); PREPARE_BALANCE_INITIAL: - my $bm_actual = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); - my $profile = $bm_actual->billing_mappings->first->billing_profile; + my $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $now); + my $profile = $bm_actual->billing_profile; if (defined $contract->contact->reseller_id && $package) { $start_mode = $package->balance_interval_start_mode; $interval_unit = $package->balance_interval_unit; @@ -678,8 +679,8 @@ sub _get_resized_balance_values { my $contract = $balance->contract; my $contract_create = NGCP::Panel::Utils::DateTime::set_local_tz($contract->create_timestamp // $contract->modify_timestamp); if (NGCP::Panel::Utils::DateTime::set_local_tz($balance->start) <= $contract_create && (NGCP::Panel::Utils::DateTime::is_infinite_future($balance->end) || NGCP::Panel::Utils::DateTime::set_local_tz($balance->end) >= $contract_create)) { - my $bm = get_actual_billing_mapping(schema => $schema, contract => $contract, now => $contract_create); #now => $balance->start); #end); !? - my $profile = $bm->billing_mappings->first->billing_profile; + my $bm = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(schema => $schema, contract => $contract, now => $contract_create); #now => $balance->start); #end); !? + my $profile = $bm->billing_profile; my $old_ratio = get_free_ratio($contract_create,NGCP::Panel::Utils::DateTime::set_local_tz($balance->start),NGCP::Panel::Utils::DateTime::set_local_tz($balance->end)); my $old_free_cash = $old_ratio * ($profile->interval_free_cash // _DEFAULT_PROFILE_FREE_CASH); my $old_free_time = $old_ratio * ($profile->interval_free_time // _DEFAULT_PROFILE_FREE_TIME); @@ -968,17 +969,6 @@ sub get_timely_range { return ($is_timely,$timely_start,$timely_end); } -sub get_actual_billing_mapping { - my %params = @_; - my ($c,$schema,$contract,$now) = @params{qw/c schema contract now/}; - $schema //= $c->model('DB'); - $now //= NGCP::Panel::Utils::DateTime::current_local; - my $contract_create = NGCP::Panel::Utils::DateTime::set_local_tz($contract->create_timestamp // $contract->modify_timestamp); - my $dtf = $schema->storage->datetime_parser; - $now = $contract_create if $now < $contract_create; #if there is no mapping starting with or before $now, it would returns the mapping with max(id): - return $schema->resultset('billing_mappings_actual')->search({ contract_id => $contract->id },{bind => [ ( $dtf->format_datetime($now) ) x 2, ($contract->id) x 2 ],})->first; -} - sub set_subscriber_lock_level { my %params = @_; my ($c,$contract,$lock_level) = @params{qw/c contract lock_level/}; @@ -1059,10 +1049,10 @@ sub add_profile_mappings { my ($c,$contract,$bm_actual,$package,$stime,$profiles,$now) = @params{qw/c contract bm_actual package stime profiles now/}; my @profiles; if ($contract->status ne 'terminated' && (scalar (@profiles = $package->$profiles->all)) > 0) { - $bm_actual //= get_actual_billing_mapping(c => $c, + $bm_actual //= NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract, now => $now); - my $product_id = $bm_actual->billing_mappings->first->product->id; + my $product_id = $bm_actual->product->id; #my $old_prepaid = $bm_actual->billing_mappings->first->billing_profile->prepaid; my @mappings_to_create = (); foreach my $mapping (@profiles) { @@ -1077,13 +1067,13 @@ sub add_profile_mappings { foreach my $mapping (@mappings_to_create) { $contract->billing_mappings->create($mapping); } - $bm_actual = get_actual_billing_mapping(c => $c, + $bm_actual = NGCP::Panel::Utils::BillingMappings::get_actual_billing_mapping(c => $c, contract => $contract, now => $now); NGCP::Panel::Utils::Subscriber::switch_prepaid_contract(c => $c, #old_prepaid => $old_prepaid, #new_prepaid => $bm_actual->billing_mappings->first->billing_profile->prepaid, - prepaid => $bm_actual->billing_mappings->first->billing_profile->prepaid, + prepaid => $bm_actual->billing_profile->prepaid, contract => $contract, ); return scalar @profiles; @@ -1422,7 +1412,7 @@ sub get_customer_datatable_cols { return ( { name => "id", search => 1, title => $c->loc("#") }, { name => "external_id", search => 1, title => $c->loc("External #") }, - #{ name => "billing_mappings_actual.billing_mappings.product.name", search => 1, title => $c->loc("Product") }, + #{ name => "product.name", search => 1, title => $c->loc("Product") }, { name => "contact.email", search => 1, title => $c->loc("Contact Email") }, { name => "status", search => 1, title => $c->loc("Status") }, ); diff --git a/share/templates/helpers/datatables.tt b/share/templates/helpers/datatables.tt index 2d1f6e4550..5da0bb633d 100644 --- a/share/templates/helpers/datatables.tt +++ b/share/templates/helpers/datatables.tt @@ -96,11 +96,16 @@ $(document).ready(function() { if(data == null) return ''; [% IF helper.custom_renderers.${f} -%] + data.escapeHtml = true; //true by default; var renderCustom = [% helper.custom_renderers.${f} -%]; [% ELSE -%] - var renderCustom = function(data, type, full) { return data; }; + var renderCustom = function(data, type, full) { data.escapeHtml = true; return data; }; [% END -%] - return String(renderCustom(data, type, full)).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');//" + var str = String(renderCustom(data, type, full)); + if (data.escapeHtml) { + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');//" + } + return str; }, "bSortable": [% IF helper.column_sortable_map.${f} %] true [% ELSE %] false [% END %] }, @@ -108,7 +113,7 @@ $(document).ready(function() { { "mRender": function ( data, type, full ) { var html = '' + '
'; - [% + [% backuri = backuri | uri; FOR button IN helper.dt_buttons; separator = '?'; @@ -123,7 +128,7 @@ $(document).ready(function() { IF button.condition; -%] if([% button.condition %]) { - [% + [% END; -%] html += @@ -135,7 +140,7 @@ $(document).ready(function() { IF button.condition; -%] } - [% + [% END; END; -%]