MT#7747 API: implement fee bulk upload

You can now do a POST to /api/billingfees/?billing_profile_id=xx
with Content-Type text/csv, and it will try to parse and insert
the file like the panel does on file upload.
gjungwirth/voicemail_number
Andreas Granig 11 years ago
parent da71415239
commit c34f5e11a0

@ -9,6 +9,7 @@ use HTTP::Status qw(:constants);
use MooseX::ClassAttribute qw(class_has);
use NGCP::Panel::Utils::DateTime;
use Path::Tiny qw(path);
use Text::CSV_XS;
BEGIN { extends 'Catalyst::Controller::ActionRole'; }
require Catalyst::ActionRole::ACL;
require Catalyst::ActionRole::CheckTrailingSlash;
@ -64,7 +65,10 @@ sub auto :Private {
my ($self, $c) = @_;
$self->set_body($c);
$self->log_request($c);
unless(defined $c->request->header('Content-Type') &&
$c->request->header('Content-Type') eq 'text/csv') {
$self->log_request($c);
}
}
sub GET :Allow {
@ -141,35 +145,53 @@ sub POST :Allow {
my $guard = $c->model('DB')->txn_scope_guard;
{
my $schema = $c->model('DB');
my $resource = $self->get_valid_post_data(
my $resource;
my $data= $self->get_valid_raw_post_data(
c => $c,
media_type => 'application/json',
media_type => [qw#application/json text/csv#],
);
last unless $resource;
last unless $data;
if($c->request->header('Content-Type') eq 'text/csv') {
$resource = $c->req->query_params;
$resource->{purge_existing} = JSON::Types::bool($resource->{purge_existing});
} else {
last unless $self->require_wellformed_json($c, 'application/json', $data);
$resource = JSON::from_json($data);
$data = undef;
}
my $reseller_id;
if($c->user->roles eq "admin") {
} elsif($c->user->roles eq "reseller") {
$reseller_id = $c->user->reseller_id;
} else {
$reseller_id = $c->user->contract->contact->reseller_id;
}
unless($resource->{billing_profile_id}) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Missing parameter 'billing_profile_id'.");
last;
}
my $profile = $schema->resultset('billing_profiles')->find($resource->{billing_profile_id});
unless($profile) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_profile_id'.");
last;
}
if($c->user->roles ne "admin" && $profile->reseller_id != $reseller_id) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_profile_id'.");
last;
}
my $form = $self->get_form($c);
my $billing_profile_id = $resource->{billing_profile_id} // undef;
unless($data) {
my $profile;
my $form = $self->get_form($c);
my $zone;
# in case of implicit zone declaration (name/detail instead of id),
# find or create the zone
if(!defined $resource->{billing_zone_id} &&
defined $resource->{billing_profile_id} &&
defined $resource->{billing_zone_zone} &&
defined $resource->{billing_zone_detail}) {
# in case of implicit zone declaration (name/detail instead of id),
# find or create the zone
if(!defined $resource->{billing_zone_id} &&
defined $resource->{billing_zone_zone} &&
defined $resource->{billing_zone_detail}) {
$profile = $schema->resultset('billing_profiles')->find($resource->{billing_profile_id});
if($profile) {
my $zone = $profile->billing_zones->find({
$zone = $profile->billing_zones->find({
zone => $resource->{billing_zone_zone},
detail => $resource->{billing_zone_detail},
});
@ -180,46 +202,101 @@ sub POST :Allow {
$resource->{billing_zone_id} = $zone->id;
delete $resource->{billing_zone_zone};
delete $resource->{billing_zone_detail};
} elsif(defined $resource->{billing_zone_id}) {
$zone = $profile->billing_zones->find($resource->{billing_zone_id});
}
unless($zone) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_zone_id'.");
last;
}
}
$resource->{billing_zone_id} //= undef;
last unless $self->validate_form(
c => $c,
resource => $resource,
form => $form,
);
$resource->{billing_profile_id} = $billing_profile_id;
last unless $self->validate_form(
c => $c,
resource => $resource,
form => $form,
);
$profile //= $schema->resultset('billing_profiles')->find($resource->{billing_profile_id});
unless($profile) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_profile_id'.");
last;
}
if($c->user->roles ne "admin" && $profile->reseller_id != $reseller_id) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_profile_id'.");
last;
}
my $zone = $profile->billing_zones->find($resource->{billing_zone_id});
unless($zone) {
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid 'billing_zone_id'.");
last;
}
my $fee;
try {
$fee = $profile->billing_fees->create($resource);
} catch($e) {
$c->log->error("failed to create billing fee: $e"); # TODO: user, message, trace, ...
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create billing fee.");
my $fee;
try {
$fee = $profile->billing_fees->create($resource);
} catch($e) {
$c->log->error("failed to create billing fee: $e"); # TODO: user, message, trace, ...
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create billing fee.");
last;
}
$guard->commit;
$c->response->status(HTTP_CREATED);
$c->response->header(Location => sprintf('%s%d', $self->dispatch_path, $fee->id));
$c->response->body(q());
last;
}
} else {
# csv bulk upload
my $csv = Text::CSV_XS->new({allow_whitespace => 1, binary => 1, keep_meta_info => 1});
my @cols = @{ $c->config->{fees_csv}->{element_order} };
if ($self->is_true($resource->{purge_existing})) {
$profile->billing_fees->delete;
}
my @fails = ();
my $linenum = 0;
my @fees = ();
my %zones = ();
try {
foreach my $line(split /\r?\n/, $data) {
++$linenum;
chomp $line;
next unless length $line;
unless($csv->parse($line)) {
push @fails, $linenum;
next;
}
my $row = {};
my @fields = $csv->fields();
unless (scalar @fields == scalar @cols) {
push @fails, $linenum;
next;
}
for(my $i = 0; $i < @cols; ++$i) {
$row->{$cols[$i]} = $fields[$i];
}
$guard->commit;
my $k = $row->{zone}.'__NGCP__'.$row->{zone_detail};
unless(exists $zones{$k}) {
my $zone = $profile->billing_zones->find_or_create({
zone => $row->{zone},
detail => $row->{zone_detail}
});
$zones{$k} = $zone->id;
}
$row->{billing_zone_id} = $zones{$k};
delete $row->{zone};
delete $row->{zone_detail};
push @fees, $row;
}
$profile->billing_fees->populate(\@fees);
my $text = $c->loc('Billing Fee successfully uploaded');
if(@fails) {
$text .= $c->loc(", but skipped the following line numbers: ") . (join ", ", @fails);
}
$c->log->info($text);
$guard->commit;
$c->response->status(HTTP_CREATED);
$c->response->body(q());
} catch($e) {
$c->log->error("failed to upload csv: $e");
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error");
last;
};
}
$c->response->status(HTTP_CREATED);
$c->response->header(Location => sprintf('%s%d', $self->dispatch_path, $fee->id));
$c->response->body(q());
}
return;
}

@ -0,0 +1,27 @@
package NGCP::Panel::Form::BillingFee::API;
use HTML::FormHandler::Moose;
extends 'NGCP::Panel::Form::BillingFee';
use Moose::Util::TypeConstraints;
has_field 'purge_existing' => (
type => 'Boolean',
element_attr => {
rel => ['tooltip'],
title => ['If fees are uploaded via text/csv bulk upload, this option defines whether to purge any existing fees for the given billing profile before inserting the new ones.']
},
default => 0,
);
has_block 'fields' => (
tag => 'div',
class => [qw/modal-body/],
render_list => [qw/purge_existing billing_zone source destination direction
onpeak_init_rate onpeak_init_interval onpeak_follow_rate
onpeak_follow_interval offpeak_init_rate offpeak_init_interval
offpeak_follow_rate offpeak_follow_interval use_free_time
/],
);
1;
# vim: set tabstop=4 expandtab:

@ -13,7 +13,7 @@ use Data::HAL::Link qw();
use HTTP::Status qw(:constants);
use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Contract;
use NGCP::Panel::Form::BillingFee qw();
use NGCP::Panel::Form::BillingFee::API;
sub item_rs {
my ($self, $c) = @_;
@ -38,7 +38,7 @@ sub item_rs {
sub get_form {
my ($self, $c) = @_;
return NGCP::Panel::Form::BillingFee->new;
return NGCP::Panel::Form::BillingFee::API->new(ctx => $c);
}
sub hal_from_fee {

Loading…
Cancel
Save