Change-Id: Ia82579179a29245d51c5ae3d3ea71054ceaf1bf0changes/80/1880/1
							parent
							
								
									161895cf7e
								
							
						
					
					
						commit
						2c54106fd0
					
				| @ -0,0 +1,198 @@ | ||||
| package NGCP::Panel::Controller::API::Vouchers; | ||||
| use Sipwise::Base; | ||||
| use namespace::sweep; | ||||
| use boolean qw(true); | ||||
| use Data::HAL qw(); | ||||
| use Data::HAL::Link qw(); | ||||
| use HTTP::Headers qw(); | ||||
| use HTTP::Status qw(:constants); | ||||
| use MooseX::ClassAttribute qw(class_has); | ||||
| use NGCP::Panel::Utils::DateTime; | ||||
| use Path::Tiny qw(path); | ||||
| use Safe::Isa qw($_isa); | ||||
| BEGIN { extends 'Catalyst::Controller::ActionRole'; } | ||||
| require Catalyst::ActionRole::ACL; | ||||
| require Catalyst::ActionRole::CheckTrailingSlash; | ||||
| require Catalyst::ActionRole::HTTPMethods; | ||||
| require Catalyst::ActionRole::RequireSSL; | ||||
| 
 | ||||
| class_has 'api_description' => ( | ||||
|     is => 'ro', | ||||
|     isa => 'Str', | ||||
|     default =>  | ||||
|         'Defines vouchers to top-up subscriber balances.', | ||||
| ); | ||||
| 
 | ||||
| class_has 'query_params' => ( | ||||
|     is => 'ro', | ||||
|     isa => 'ArrayRef', | ||||
|     default => sub {[ | ||||
|         { | ||||
|             param => 'reseller_id', | ||||
|             description => 'Filter for vouchers belonging to a specific reseller', | ||||
|             query => { | ||||
|                 first => sub { | ||||
|                     my $q = shift; | ||||
|                     { reseller_id => $q }; | ||||
|                 }, | ||||
|                 second => sub {}, | ||||
|             }, | ||||
|         }, | ||||
|     ]}, | ||||
| ); | ||||
| 
 | ||||
| with 'NGCP::Panel::Role::API::Vouchers'; | ||||
| 
 | ||||
| class_has('resource_name', is => 'ro', default => 'vouchers'); | ||||
| class_has('dispatch_path', is => 'ro', default => '/api/vouchers/'); | ||||
| class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-vouchers'); | ||||
| 
 | ||||
| __PACKAGE__->config( | ||||
|     action => { | ||||
|         map { $_ => { | ||||
|             ACLDetachTo => '/api/root/invalid_user', | ||||
|             AllowedRole => [qw/admin reseller/], | ||||
|             Args => 0, | ||||
|             Does => [qw(ACL CheckTrailingSlash RequireSSL)], | ||||
|             Method => $_, | ||||
|             Path => __PACKAGE__->dispatch_path, | ||||
|         } } @{ __PACKAGE__->allowed_methods } | ||||
|     }, | ||||
|     action_roles => [qw(HTTPMethods)], | ||||
| ); | ||||
| 
 | ||||
| sub auto :Private { | ||||
|     my ($self, $c) = @_; | ||||
| 
 | ||||
|     $self->set_body($c); | ||||
|     $self->log_request($c); | ||||
| } | ||||
| 
 | ||||
| sub GET :Allow { | ||||
|     my ($self, $c) = @_; | ||||
|     my $page = $c->request->params->{page} // 1; | ||||
|     my $rows = $c->request->params->{rows} // 10; | ||||
|     { | ||||
|         my $items = $self->item_rs($c); | ||||
|         (my $total_count, $items) = $self->paginate_order_collection($c, $items); | ||||
|         my (@embedded, @links); | ||||
|         my $form = $self->get_form($c); | ||||
|         for my $item ($items->search({}, {prefetch => ['reseller']})->all) { | ||||
|             push @embedded, $self->hal_from_item($c, $item, $form); | ||||
|             push @links, Data::HAL::Link->new( | ||||
|                 relation => 'ngcp:'.$self->resource_name, | ||||
|                 href     => sprintf('/%s%d', $c->request->path, $item->id), | ||||
|             ); | ||||
|         } | ||||
|         push @links, | ||||
|             Data::HAL::Link->new( | ||||
|                 relation => 'curies', | ||||
|                 href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', | ||||
|                 name => 'ngcp', | ||||
|                 templated => true, | ||||
|             ), | ||||
|             Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), | ||||
|             Data::HAL::Link->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s', $c->request->path, $page, $rows)); | ||||
|         if(($total_count / $rows) > $page ) { | ||||
|             push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page + 1, $rows)); | ||||
|         } | ||||
|         if($page > 1) { | ||||
|             push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows)); | ||||
|         } | ||||
| 
 | ||||
|         my $hal = Data::HAL->new( | ||||
|             embedded => [@embedded], | ||||
|             links => [@links], | ||||
|         ); | ||||
|         $hal->resource({ | ||||
|             total_count => $total_count, | ||||
|         }); | ||||
|         my $response = HTTP::Response->new(HTTP_OK, undef,  | ||||
|             HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); | ||||
|         $c->response->headers($response->headers); | ||||
|         $c->response->body($response->content); | ||||
|         return; | ||||
|     } | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub HEAD :Allow { | ||||
|     my ($self, $c) = @_; | ||||
|     $c->forward(qw(GET)); | ||||
|     $c->response->body(q()); | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub OPTIONS :Allow { | ||||
|     my ($self, $c) = @_; | ||||
|     my $allowed_methods = $self->allowed_methods_filtered($c); | ||||
|     $c->response->headers(HTTP::Headers->new( | ||||
|         Allow => $allowed_methods->join(', '), | ||||
|         Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, | ||||
|     )); | ||||
|     $c->response->content_type('application/json'); | ||||
|     $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub POST :Allow { | ||||
|     my ($self, $c) = @_; | ||||
| 
 | ||||
|     my $guard = $c->model('DB')->txn_scope_guard; | ||||
|     { | ||||
|         my $resource = $self->get_valid_post_data( | ||||
|             c => $c,  | ||||
|             media_type => 'application/json', | ||||
|         ); | ||||
|         last unless $resource; | ||||
| 
 | ||||
|         my $form = $self->get_form($c); | ||||
|         last unless $self->validate_form( | ||||
|             c => $c, | ||||
|             resource => $resource, | ||||
|             form => $form, | ||||
|         ); | ||||
|         if($c->user->roles eq "admin") { | ||||
|         } elsif($c->user->roles eq "reseller") { | ||||
|             $resource->{reseller_id} = $c->user->reseller_id; | ||||
|         } | ||||
| 
 | ||||
|         my $item; | ||||
|         my $code = $self->encrypt_code($c, $resource->{code}); | ||||
|         $item = $c->model('DB')->resultset('vouchers')->find({ | ||||
|             reseller_id => $resource->{reseller_id}, | ||||
|             code => $code, | ||||
|         }); | ||||
|         if($item) { | ||||
|             $c->log->error("voucher with code '$$resource{code}' already exists for reseller_id '$$resource{reseller_id}'"); # TODO: user, message, trace, ... | ||||
|             $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Voucher with this code already exists for this reseller"); | ||||
|             last; | ||||
|         } | ||||
|         $resource->{code} = $code; | ||||
|         $resource->{created_at} = NGCP::Panel::Utils::DateTime::current_local; | ||||
| 
 | ||||
| 
 | ||||
|         try { | ||||
|             $item = $c->model('DB')->resultset('vouchers')->create($resource); | ||||
|         } catch($e) { | ||||
|             $c->log->error("failed to create voucher: $e"); # TODO: user, message, trace, ... | ||||
|             $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create voucher."); | ||||
|             last; | ||||
|         } | ||||
| 
 | ||||
|         $guard->commit; | ||||
| 
 | ||||
|         $c->response->status(HTTP_CREATED); | ||||
|         $c->response->header(Location => sprintf('/%s%d', $c->request->path, $item->id)); | ||||
|         $c->response->body(q()); | ||||
|     } | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub end : Private { | ||||
|     my ($self, $c) = @_; | ||||
| 
 | ||||
|     $self->log_response($c); | ||||
| } | ||||
| 
 | ||||
| # vim: set tabstop=4 expandtab: | ||||
| @ -0,0 +1,191 @@ | ||||
| package NGCP::Panel::Controller::API::VouchersItem; | ||||
| use Sipwise::Base; | ||||
| use namespace::sweep; | ||||
| use HTTP::Headers qw(); | ||||
| use HTTP::Status qw(:constants); | ||||
| use MooseX::ClassAttribute qw(class_has); | ||||
| use NGCP::Panel::Utils::DateTime; | ||||
| use NGCP::Panel::Utils::ValidateJSON qw(); | ||||
| use Path::Tiny qw(path); | ||||
| use Safe::Isa qw($_isa); | ||||
| BEGIN { extends 'Catalyst::Controller::ActionRole'; } | ||||
| require Catalyst::ActionRole::ACL; | ||||
| require Catalyst::ActionRole::HTTPMethods; | ||||
| require Catalyst::ActionRole::RequireSSL; | ||||
| 
 | ||||
| with 'NGCP::Panel::Role::API::Vouchers'; | ||||
| 
 | ||||
| class_has('resource_name', is => 'ro', default => 'vouchers'); | ||||
| class_has('dispatch_path', is => 'ro', default => '/api/vouchers/'); | ||||
| class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-vouchers'); | ||||
| 
 | ||||
| __PACKAGE__->config( | ||||
|     action => { | ||||
|         map { $_ => { | ||||
|             ACLDetachTo => '/api/root/invalid_user', | ||||
|             AllowedRole => [qw/admin reseller/], | ||||
|             Args => 1, | ||||
|             Does => [qw(ACL RequireSSL)], | ||||
|             Method => $_, | ||||
|             Path => __PACKAGE__->dispatch_path, | ||||
|         } } @{ __PACKAGE__->allowed_methods } | ||||
|     }, | ||||
|     action_roles => [qw(HTTPMethods)], | ||||
| ); | ||||
| 
 | ||||
| sub auto :Private { | ||||
|     my ($self, $c) = @_; | ||||
| 
 | ||||
|     $self->set_body($c); | ||||
|     $self->log_request($c); | ||||
| } | ||||
| 
 | ||||
| sub GET :Allow { | ||||
|     my ($self, $c, $id) = @_; | ||||
|     { | ||||
|         last unless $self->valid_id($c, $id); | ||||
|         my $item = $self->item_by_id($c, $id); | ||||
|         last unless $self->resource_exists($c, voucher => $item); | ||||
| 
 | ||||
|         my $hal = $self->hal_from_item($c, $item); | ||||
| 
 | ||||
|         my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( | ||||
|             (map { # XXX Data::HAL must be able to generate links with multiple relations | ||||
|                 s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|; | ||||
|                 s/rel=self/rel="item self"/; | ||||
|                 $_ | ||||
|             } $hal->http_headers), | ||||
|         ), $hal->as_json); | ||||
|         $c->response->headers($response->headers); | ||||
|         $c->response->body($response->content); | ||||
|         return; | ||||
|     } | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub HEAD :Allow { | ||||
|     my ($self, $c, $id) = @_; | ||||
|     $c->forward(qw(GET)); | ||||
|     $c->response->body(q()); | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub OPTIONS :Allow { | ||||
|     my ($self, $c, $id) = @_; | ||||
|     my $allowed_methods = $self->allowed_methods_filtered($c); | ||||
|     $c->response->headers(HTTP::Headers->new( | ||||
|         Allow => $allowed_methods->join(', '), | ||||
|         Accept_Patch => 'application/json-patch+json', | ||||
|     )); | ||||
|     $c->response->content_type('application/json'); | ||||
|     $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub PATCH :Allow { | ||||
|     my ($self, $c, $id) = @_; | ||||
|     my $guard = $c->model('DB')->txn_scope_guard; | ||||
|     { | ||||
|         my $preference = $self->require_preference($c); | ||||
|         last unless $preference; | ||||
| 
 | ||||
|         my $json = $self->get_valid_patch_data( | ||||
|             c => $c,  | ||||
|             id => $id, | ||||
|             media_type => 'application/json-patch+json', | ||||
|         ); | ||||
|         last unless $json; | ||||
| 
 | ||||
|         my $item = $self->item_by_id($c, $id); | ||||
|         last unless $self->resource_exists($c, voucher => $item); | ||||
|         my $old_resource = { $item->get_inflated_columns }; | ||||
|         my $resource = $self->apply_patch($c, $old_resource, $json); | ||||
|         last unless $resource; | ||||
| 
 | ||||
|         my $form = $self->get_form($c); | ||||
|         $item = $self->update_item($c, $item, $old_resource, $resource, $form); | ||||
|         last unless $item; | ||||
|          | ||||
|         $guard->commit; | ||||
| 
 | ||||
|         if ('minimal' eq $preference) { | ||||
|             $c->response->status(HTTP_NO_CONTENT); | ||||
|             $c->response->header(Preference_Applied => 'return=minimal'); | ||||
|             $c->response->body(q()); | ||||
|         } else { | ||||
|             my $hal = $self->hal_from_item($c, $item, $form); | ||||
|             my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( | ||||
|                 $hal->http_headers, | ||||
|             ), $hal->as_json); | ||||
|             $c->response->headers($response->headers); | ||||
|             $c->response->header(Preference_Applied => 'return=representation'); | ||||
|             $c->response->body($response->content); | ||||
|         } | ||||
|     } | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub PUT :Allow { | ||||
|     my ($self, $c, $id) = @_; | ||||
|     my $guard = $c->model('DB')->txn_scope_guard; | ||||
|     { | ||||
|         my $preference = $self->require_preference($c); | ||||
|         last unless $preference; | ||||
| 
 | ||||
|         my $item = $self->item_by_id($c, $id); | ||||
|         last unless $self->resource_exists($c, voucher => $item); | ||||
|         my $resource = $self->get_valid_put_data( | ||||
|             c => $c, | ||||
|             id => $id, | ||||
|             media_type => 'application/json', | ||||
|         ); | ||||
|         last unless $resource; | ||||
|         my $old_resource = { $item->get_inflated_columns }; | ||||
| 
 | ||||
|         my $form = $self->get_form($c); | ||||
|         $item = $self->update_item($c, $item, $old_resource, $resource, $form); | ||||
|         last unless $item; | ||||
| 
 | ||||
|         $guard->commit;  | ||||
| 
 | ||||
|         if ('minimal' eq $preference) { | ||||
|             $c->response->status(HTTP_NO_CONTENT); | ||||
|             $c->response->header(Preference_Applied => 'return=minimal'); | ||||
|             $c->response->body(q()); | ||||
|         } else { | ||||
|             my $hal = $self->hal_from_item($c, $item, $form); | ||||
|             my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( | ||||
|                 $hal->http_headers, | ||||
|             ), $hal->as_json); | ||||
|             $c->response->headers($response->headers); | ||||
|             $c->response->header(Preference_Applied => 'return=representation'); | ||||
|             $c->response->body($response->content); | ||||
|         } | ||||
|     } | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub DELETE :Allow { | ||||
|     my ($self, $c, $id) = @_; | ||||
| 
 | ||||
|     my $guard = $c->model('DB')->txn_scope_guard; | ||||
|     { | ||||
|         my $item = $self->item_by_id($c, $id); | ||||
|         last unless $self->resource_exists($c, voucher => $item); | ||||
|         $item->delete; | ||||
| 
 | ||||
|         $guard->commit; | ||||
| 
 | ||||
|         $c->response->status(HTTP_NO_CONTENT); | ||||
|         $c->response->body(q()); | ||||
|     } | ||||
|     return; | ||||
| } | ||||
| 
 | ||||
| sub end : Private { | ||||
|     my ($self, $c) = @_; | ||||
| 
 | ||||
|     $self->log_response($c); | ||||
| } | ||||
| 
 | ||||
| # vim: set tabstop=4 expandtab: | ||||
| @ -0,0 +1,16 @@ | ||||
| package NGCP::Panel::Form::Voucher::AdminAPI; | ||||
| 
 | ||||
| use HTML::FormHandler::Moose; | ||||
| extends 'NGCP::Panel::Form::Voucher::ResellerAPI'; | ||||
| 
 | ||||
| has_field 'reseller' => ( | ||||
|     type => '+NGCP::Panel::Field::Reseller', | ||||
|     validate_when_empty => 1, | ||||
|     element_attr => { | ||||
|         rel => ['tooltip'], | ||||
|         title => ['The reseller id this voucher belongs to.'] | ||||
|     }, | ||||
| ); | ||||
| 
 | ||||
| 1; | ||||
| # vim: set tabstop=4 expandtab: | ||||
| @ -0,0 +1,69 @@ | ||||
| package NGCP::Panel::Form::Voucher::ResellerAPI; | ||||
| 
 | ||||
| use HTML::FormHandler::Moose; | ||||
| extends 'HTML::FormHandler'; | ||||
| use Moose::Util::TypeConstraints; | ||||
| 
 | ||||
| use HTML::FormHandler::Widget::Block::Bootstrap; | ||||
| 
 | ||||
| has '+widget_wrapper' => ( default => 'Bootstrap' ); | ||||
| has_field 'submitid' => ( type => 'Hidden' ); | ||||
| sub build_render_list {[qw/submitid fields actions/]} | ||||
| sub build_form_element_class { [qw/form-horizontal/] } | ||||
| 
 | ||||
| has_field 'id' => ( | ||||
|     type => 'Hidden' | ||||
| ); | ||||
| 
 | ||||
| has_field 'code' => ( | ||||
|     type => 'Text', | ||||
|     required => 1, | ||||
|     maxlength => 128, | ||||
|     element_attr => { | ||||
|         rel => ['tooltip'], | ||||
|         title => ['The voucher code.'] | ||||
|     }, | ||||
| ); | ||||
| 
 | ||||
| has_field 'amount' => ( | ||||
|     type => 'Money', | ||||
|     required => 1, | ||||
|     element_attr => { | ||||
|         rel => ['tooltip'], | ||||
|         title => ['The amount of the voucher in cents of Euro/USD/etc.'] | ||||
|     }, | ||||
|     default => '0', | ||||
| ); | ||||
| 
 | ||||
| has_field 'valid_until' => ( | ||||
|     type => '+NGCP::Panel::Field::DateTime', | ||||
|     required => 1, | ||||
|     element_attr => { | ||||
|         rel => ['tooltip'], | ||||
|         title => ['The date until this voucher is valid (YYYY-MM-DD hh:mm:ss).'] | ||||
|     }, | ||||
| ); | ||||
| 
 | ||||
| has_field 'customer' => ( | ||||
|     type => '+NGCP::Panel::Field::CustomerContract', | ||||
|     element_attr => { | ||||
|         rel => ['tooltip'], | ||||
|         title => ['The customer contract this voucher can be used by (optional).'] | ||||
|     }, | ||||
| ); | ||||
| 
 | ||||
| sub validate_valid_until { | ||||
|     my ($self, $field) = @_; | ||||
| 
 | ||||
|     unless($field->value =~ /^(\d{4})\-\d{2}\-\d{2}(T| )\d{2}:\d{2}:\d{2}$/) { | ||||
|         my $err_msg = 'Invalid date format, must be YYYY-MM-DD'; | ||||
|         $field->add_error($err_msg); | ||||
|     } | ||||
|     if(int($1) > 2037) { | ||||
|         my $err_msg = 'Invalid date format, YYYY must not be greater than 2037'; | ||||
|         $field->add_error($err_msg); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 1 | ||||
| # vim: set tabstop=4 expandtab: | ||||
| @ -0,0 +1,151 @@ | ||||
| package NGCP::Panel::Role::API::Vouchers; | ||||
| use Moose::Role; | ||||
| use Sipwise::Base; | ||||
| use Crypt::Rijndael; | ||||
| use MIME::Base64; | ||||
| with 'NGCP::Panel::Role::API' => { | ||||
|     -alias       =>{ item_rs  => '_item_rs', }, | ||||
|     -excludes    => [ 'item_rs' ], | ||||
| }; | ||||
| 
 | ||||
| use boolean qw(true); | ||||
| use TryCatch; | ||||
| use Data::HAL qw(); | ||||
| use Data::HAL::Link qw(); | ||||
| use HTTP::Status qw(:constants); | ||||
| use NGCP::Panel::Form::Voucher::AdminAPI; | ||||
| use NGCP::Panel::Form::Voucher::ResellerAPI; | ||||
| 
 | ||||
| sub item_rs { | ||||
|     my ($self, $c) = @_; | ||||
| 
 | ||||
|     my $item_rs = $c->model('DB')->resultset('vouchers'); | ||||
|     if($c->user->roles eq "admin") { | ||||
|     } elsif($c->user->roles eq "reseller") { | ||||
|         $item_rs = $item_rs->search({ reseller_id => $c->user->reseller_id }); | ||||
|     } | ||||
|     return $item_rs; | ||||
| } | ||||
| 
 | ||||
| sub get_form { | ||||
|     my ($self, $c) = @_; | ||||
|     if($c->user->roles eq "admin") { | ||||
|         return NGCP::Panel::Form::Voucher::AdminAPI->new; | ||||
|     } elsif($c->user->roles eq "reseller") { | ||||
|         return NGCP::Panel::Form::Voucher::ResellerAPI->new; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| sub hal_from_item { | ||||
|     my ($self, $c, $item, $form) = @_; | ||||
|     my %resource = $item->get_inflated_columns; | ||||
| 
 | ||||
|     my $hal = Data::HAL->new( | ||||
|         links => [ | ||||
|             Data::HAL::Link->new( | ||||
|                 relation => 'curies', | ||||
|                 href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', | ||||
|                 name => 'ngcp', | ||||
|                 templated => true, | ||||
|             ), | ||||
|             Data::HAL::Link->new(relation => 'collection', href => sprintf("/api/%s/", $self->resource_name)), | ||||
|             Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), | ||||
|             Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), | ||||
|             Data::HAL::Link->new(relation => 'ngcp:resellers', href => sprintf("/api/resellers/%d", $item->reseller_id)), | ||||
|         ], | ||||
|         relation => 'ngcp:'.$self->resource_name, | ||||
|     ); | ||||
| 
 | ||||
|     $form //= $self->get_form($c); | ||||
| 
 | ||||
|     $self->validate_form( | ||||
|         c => $c, | ||||
|         resource => \%resource, | ||||
|         form => $form, | ||||
|         run => 0, | ||||
|     ); | ||||
| 
 | ||||
|     $resource{valid_until} = $item->valid_until->ymd('-') . ' ' . $item->valid_until->hms(':'); | ||||
|     $resource{code} = $self->decrypt_code($c, $item->code); | ||||
|     $resource{id} = int($item->id); | ||||
|     $hal->resource({%resource}); | ||||
|     return $hal; | ||||
| } | ||||
| 
 | ||||
| sub item_by_id { | ||||
|     my ($self, $c, $id) = @_; | ||||
|     my $item_rs = $self->item_rs($c); | ||||
|     return $item_rs->find($id); | ||||
| } | ||||
| 
 | ||||
| sub encrypt_code { | ||||
|     my ($self, $c, $plain) = @_; | ||||
| 
 | ||||
|     my $key = $c->config->{vouchers}->{key}; | ||||
|     my $iv = $c->config->{vouchers}->{iv}; | ||||
| 
 | ||||
|     # pkcs#5 padding to 16 bytes blocksize | ||||
|     my $pad = 16 - (length $plain) % 16; | ||||
|     $plain .= pack('C', $pad) x $pad; | ||||
| 
 | ||||
|     my $cipher = Crypt::Rijndael->new( | ||||
|         $key, | ||||
|         Crypt::Rijndael::MODE_CBC() | ||||
|     ); | ||||
|     $cipher->set_iv($iv); | ||||
|     my $crypted = $cipher->encrypt($plain); | ||||
|     my $b64 = encode_base64($crypted, ''); | ||||
|     return $b64; | ||||
| } | ||||
| 
 | ||||
| sub decrypt_code { | ||||
|     my ($self, $c, $code) = @_; | ||||
| 
 | ||||
|     my $key = $c->config->{vouchers}->{key}; | ||||
|     my $iv = $c->config->{vouchers}->{iv}; | ||||
| 
 | ||||
|     my $cipher = Crypt::Rijndael->new( | ||||
|         $key, | ||||
|         Crypt::Rijndael::MODE_CBC() | ||||
|     ); | ||||
|     $cipher->set_iv($iv); | ||||
|     my $crypted = decode_base64($code); | ||||
|     my $plain = $cipher->decrypt($crypted) . ""; | ||||
|     # remove padding | ||||
|     $plain =~ s/[\x01-\x1e]*$//; | ||||
|     return $plain; | ||||
| } | ||||
| 
 | ||||
| sub update_item { | ||||
|     my ($self, $c, $item, $old_resource, $resource, $form) = @_; | ||||
| 
 | ||||
|     $form //= $self->get_form($c); | ||||
|     return unless $self->validate_form( | ||||
|         c => $c, | ||||
|         form => $form, | ||||
|         resource => $resource, | ||||
|     ); | ||||
|     if($c->user->roles eq "admin") { | ||||
|     } elsif($c->user->roles eq "reseller") { | ||||
|         $resource->{reseller_id} = $c->user->reseller_id; | ||||
|     } | ||||
| 
 | ||||
|     my $code = $self->encrypt_code($c, $resource->{code}); | ||||
|     my $dup_item = $c->model('DB')->resultset('vouchers')->find({ | ||||
|         reseller_id => $resource->{reseller_id}, | ||||
|         code => $code, | ||||
|     }); | ||||
|     if($dup_item && $dup_item->id != $item->id) { | ||||
|         $c->log->error("voucher with code '$$resource{code}' already exists for reseller_id '$$resource{reseller_id}'"); # TODO: user, message, trace, ... | ||||
|         $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Voucher with this code already exists for this reseller"); | ||||
|         return; | ||||
|     } | ||||
|     $resource->{code} = $code; | ||||
| 
 | ||||
|     $item->update($resource); | ||||
| 
 | ||||
|     return $item; | ||||
| } | ||||
| 
 | ||||
| 1; | ||||
| # vim: set tabstop=4 expandtab: | ||||
| @ -0,0 +1,186 @@ | ||||
| use Sipwise::Base; | ||||
| use Net::Domain qw(hostfqdn); | ||||
| use LWP::UserAgent; | ||||
| use JSON qw(); | ||||
| use Test::More; | ||||
| use Storable qw(); | ||||
| use Data::Printer; | ||||
| 
 | ||||
| use JSON::PP; | ||||
| use LWP::Debug; | ||||
| 
 | ||||
| BEGIN { | ||||
|     unshift(@INC,'../lib'); | ||||
| } | ||||
| 
 | ||||
| my $json = JSON::PP->new(); | ||||
| $json->allow_blessed(1); | ||||
| $json->convert_blessed(1); | ||||
| 
 | ||||
| my $is_local_env = $ENV{LOCAL_TEST} // 0; | ||||
| my $mysql_sqlstrict = 1; #https://bugtracker.sipwise.com/view.php?id=12565 | ||||
| 
 | ||||
| use Config::General; | ||||
| my $catalyst_config; | ||||
| if ($is_local_env) { | ||||
|     my $panel_config; | ||||
|     for my $path(qw#../ngcp_panel.conf ngcp_panel.conf#) { | ||||
|         if(-f $path) { | ||||
|             $panel_config = $path; | ||||
|             last; | ||||
|         } | ||||
|     } | ||||
|     $panel_config //= '../ngcp_panel.conf'; | ||||
|     $catalyst_config = Config::General->new($panel_config);    | ||||
| } else { | ||||
|     #taken 1:1 from /lib/NGCP/Panel.pm | ||||
|     my $panel_config; | ||||
|     for my $path(qw#/etc/ngcp-panel/ngcp_panel.conf etc/ngcp_panel.conf ngcp_panel.conf#) { | ||||
|         if(-f $path) { | ||||
|             $panel_config = $path; | ||||
|             last; | ||||
|         } | ||||
|     } | ||||
|     $panel_config //= 'ngcp_panel.conf'; | ||||
|     $catalyst_config = Config::General->new($panel_config);    | ||||
| } | ||||
| my %config = $catalyst_config->getall(); | ||||
| 
 | ||||
| my $uri = $ENV{CATALYST_SERVER} || ('https://'.hostfqdn.':4443'); | ||||
| 
 | ||||
| my $valid_ssl_client_cert = $ENV{API_SSL_CLIENT_CERT} ||  | ||||
|     "/etc/ngcp-panel/api_ssl/NGCP-API-client-certificate.pem"; | ||||
| my $valid_ssl_client_key = $ENV{API_SSL_CLIENT_KEY} || | ||||
|     $valid_ssl_client_cert; | ||||
| my $ssl_ca_cert = $ENV{API_SSL_CA_CERT} || "/etc/ngcp-panel/api_ssl/api_ca.crt"; | ||||
| 
 | ||||
| my ($ua, $req, $res); | ||||
| $ua = LWP::UserAgent->new; | ||||
| 
 | ||||
| if ($is_local_env) { | ||||
|     $ua->ssl_opts( | ||||
|         verify_hostname => 0, | ||||
|     ); | ||||
|     my $realm = $uri; $realm =~ s/^https?:\/\///; | ||||
|     $ua->credentials($realm, "api_admin_http", 'administrator', 'administrator'); | ||||
|     #$ua->timeout(500); #useless, need to change the nginx timeout | ||||
| } else { | ||||
|     $ua->ssl_opts( | ||||
|         SSL_cert_file => $valid_ssl_client_cert, | ||||
|         SSL_key_file  => $valid_ssl_client_key, | ||||
|         SSL_ca_file   => $ssl_ca_cert, | ||||
|     );     | ||||
| } | ||||
| 
 | ||||
| my $t = time; | ||||
| my $default_reseller_id = 1; | ||||
| 
 | ||||
| test_voucher(); | ||||
| done_testing(); | ||||
| 
 | ||||
| 
 | ||||
| sub test_voucher { | ||||
|     my $code = 'testcode'.$t; | ||||
|     my $voucher = { | ||||
|         amount => 100, | ||||
|         code => $code, | ||||
|         customer_id => undef, | ||||
|         reseller_id => $default_reseller_id, | ||||
|         valid_until => '2037-01-01 12:00:00', | ||||
|     }; | ||||
|     p $voucher; | ||||
|     $req = HTTP::Request->new('POST', $uri.'/api/vouchers/'); | ||||
|     $req->header('Content-Type' => 'application/json'); | ||||
|     $req->content(JSON::to_json($voucher)); | ||||
|     $res = $ua->request($req); | ||||
|     is($res->code, 201, _get_request_test_message("POST test voucher")); | ||||
|     my $voucher_uri = $uri.'/'.$res->header('Location'); | ||||
|     $req = HTTP::Request->new('GET', $voucher_uri); | ||||
|     $res = $ua->request($req); | ||||
|     is($res->code, 200, _get_request_test_message("fetch POSTed test voucher")); | ||||
|     my $post_voucher = JSON::from_json($res->decoded_content); | ||||
|     delete $post_voucher->{_links}; | ||||
|     my $voucher_id = delete $post_voucher->{id}; | ||||
|     is_deeply($voucher, $post_voucher, "check POSTed voucher against fetched"); | ||||
|     $post_voucher->{id} = $voucher_id; | ||||
| 
 | ||||
|     $req = HTTP::Request->new('PUT', $voucher_uri); | ||||
|     $req->header('Content-Type' => 'application/json'); | ||||
|     $req->header('Prefer' => 'return=representation'); | ||||
|     $req->content(JSON::to_json($post_voucher)); | ||||
|     $res = $ua->request($req); | ||||
|     is($res->code, 200, _get_request_test_message("PUT test voucher")); | ||||
|     $req = HTTP::Request->new('GET', $voucher_uri); | ||||
|     $res = $ua->request($req); | ||||
|     is($res->code, 200, _get_request_test_message("fetch PUT test voucher")); | ||||
|     my $put_voucher = JSON::from_json($res->decoded_content); | ||||
|     delete $put_voucher->{_links}; | ||||
|     $voucher_id = delete $put_voucher->{id}; | ||||
|     is_deeply($voucher, $put_voucher, "check PUTed voucher against POSTed voucher"); | ||||
| 
 | ||||
|     p $put_voucher; | ||||
|     $req = HTTP::Request->new('POST', $uri.'/api/vouchers/'); | ||||
|     $req->header('Content-Type' => 'application/json'); | ||||
|     $req->content(JSON::to_json($put_voucher)); | ||||
|     $res = $ua->request($req); | ||||
|     is($res->code, 422, _get_request_test_message("POST same voucher code again")); | ||||
| 
 | ||||
|     $put_voucher->{id} = $voucher_id; | ||||
| 
 | ||||
|      | ||||
| #    $req = HTTP::Request->new('PATCH', $billingzone_uri); | ||||
| #    $req->header('Content-Type' => 'application/json-patch+json'); | ||||
| #    $req->header('Prefer' => 'return=representation'); | ||||
| #    $req->content(JSON::to_json( | ||||
| #        [ { op => 'replace', path => '/zone', value => 'AT' } ] | ||||
| #    )); | ||||
| #    $res = $ua->request($req); | ||||
| #    is($res->code, 200, _get_request_test_message("PATCH test billingzone")); | ||||
| #    $req = HTTP::Request->new('GET', $billingzone_uri); | ||||
| #    $res = $ua->request($req); | ||||
| #    is($res->code, 200, _get_request_test_message("fetch PATCHed test billingzone")); | ||||
| #    $billingzone = JSON::from_json($res->decoded_content); | ||||
| 
 | ||||
| 
 | ||||
|     # mysql has an issue with datetime overruns,check for max date | ||||
|     $req = HTTP::Request->new('PATCH', $voucher_uri); | ||||
|     $req->header('Content-Type' => 'application/json-patch+json'); | ||||
|     $req->header('Prefer' => 'return=representation'); | ||||
|     $req->content(JSON::to_json( | ||||
|         [ { op => 'replace', path => '/valid_until', value => '2099-01-01 00:00:00' } ] | ||||
|     )); | ||||
|     $res = $ua->request($req); | ||||
|     is($res->code, 422, _get_request_test_message("PATCH too far valid_until in voucher")); | ||||
| 
 | ||||
|     $req = HTTP::Request->new('DELETE', $voucher_uri); | ||||
|     $res = $ua->request($req); | ||||
|     is($res->code, 204, _get_request_test_message("delete POSTed test voucher")); | ||||
|     $req = HTTP::Request->new('GET', $voucher_uri); | ||||
|     $res = $ua->request($req); | ||||
|     is($res->code, 404, _get_request_test_message("fetch DELETEd test voucher")); | ||||
| } | ||||
| 
 | ||||
| sub _to_json { | ||||
|     return $json->encode(shift); | ||||
| } | ||||
| 
 | ||||
| sub _from_json { | ||||
|     return $json->decode(shift); | ||||
| } | ||||
| 
 | ||||
| sub _get_request_test_message { | ||||
|     my ($message) = @_; | ||||
|     my $code = $res->code; | ||||
|     if ($code == 200 || $code == 201 || $code == 204) { | ||||
|         return $message; | ||||
|     } else { | ||||
|         my $error_content = _from_json($res->content); | ||||
|         if (defined $error_content && defined $error_content->{message}) { | ||||
|             return $message . ' (' . $res->message . ': ' . $error_content->{message} . ')'; | ||||
|         } else { | ||||
|             return $message . ' (' . $res->message . ')'; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| # vim: set tabstop=4 expandtab: | ||||
					Loading…
					
					
				
		Reference in new issue