= %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 0343493859..31c597d7cb 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);
@@ -971,17 +972,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/};
@@ -1062,10 +1052,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) {
@@ -1080,13 +1070,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;
@@ -1425,7 +1415,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;
-%]