Bug 32030: ERM - Licenses
authorJonathan Druart <jonathan.druart@bugs.koha-community.org>
Tue, 3 May 2022 16:30:38 +0000 (18:30 +0200)
committerTomas Cohen Arazi <tomascohen@theke.io>
Tue, 8 Nov 2022 12:43:44 +0000 (09:43 -0300)
Signed-off-by: Jonathan Field <jonathan.field@ptfs-europe.com>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
26 files changed:
Koha/ERM/Agreement/License.pm [new file with mode: 0644]
Koha/ERM/Agreement/Licenses.pm [new file with mode: 0644]
Koha/ERM/License.pm [new file with mode: 0644]
Koha/ERM/Licenses.pm [new file with mode: 0644]
Koha/REST/V1/ERM/Licenses.pm [new file with mode: 0644]
api/v1/swagger/definitions/erm_license.yaml [new file with mode: 0644]
api/v1/swagger/definitions/erm_license_agreement.yaml [new file with mode: 0644]
api/v1/swagger/paths/erm_licenses.yaml [new file with mode: 0644]
api/v1/swagger/swagger.yaml
cypress/integration/Licenses_spec.ts [new file with mode: 0644]
installer/data/mysql/mandatory/auth_val_cat.sql
koha-tmpl/intranet-tmpl/prog/en/modules/erm/erm.tt
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonDelete.vue [deleted file]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonEdit.vue [deleted file]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/ButtonDelete.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/ButtonEdit.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/ERMMain.vue
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/Licenses.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesFormAdd.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesFormConfirmDelete.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesList.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesShow.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesToolbar.vue [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/js/vue/main-erm.ts
t/db_dependent/api/v1/erm_licenses.t [new file with mode: 0755]

diff --git a/Koha/ERM/Agreement/License.pm b/Koha/ERM/Agreement/License.pm
new file mode 100644 (file)
index 0000000..9f34ae4
--- /dev/null
@@ -0,0 +1,71 @@
+package Koha::ERM::Agreement::License;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Koha::Database;
+
+use Koha::Agreements;
+use Koha::Licenses;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::ERM::Agreement::License - Koha Agreement License Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 license
+
+Return the license
+
+=cut
+
+sub license {
+    my ( $self ) = @_;
+    my $license_rs = $self->_result->license;
+    return Koha::ERM::License->_new_from_dbic($license_rs);
+}
+
+=head3 agreement
+
+Return the agreement
+
+=cut
+
+sub agreement {
+    my ( $self ) = @_;
+    my $agreement_rs = $self->_result->agreement;
+    return Koha::ERM::Agreement->_new_from_dbic($agreement_rs);
+}
+
+=head2 Internal methods
+
+=head3 _type
+
+=cut
+
+sub _type {
+    return 'ErmAgreementLicense';
+}
+
+1;
diff --git a/Koha/ERM/Agreement/Licenses.pm b/Koha/ERM/Agreement/Licenses.pm
new file mode 100644 (file)
index 0000000..61d9e4c
--- /dev/null
@@ -0,0 +1,53 @@
+package Koha::ERM::Agreement::Licenses;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+
+use Koha::Database;
+
+use Koha::ERM::Agreement::License;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::ERM::Agreement::Licenses- Koha Agreement License Object set class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'ErmAgreementLicense';
+}
+
+=head3 object_class
+
+=cut
+
+sub object_class {
+    return 'Koha::ERM::Agreement::License';
+}
+
+1;
diff --git a/Koha/ERM/License.pm b/Koha/ERM/License.pm
new file mode 100644 (file)
index 0000000..cd798ff
--- /dev/null
@@ -0,0 +1,44 @@
+package Koha::ERM::License;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Koha::Database;
+
+use base qw(Koha::Object);
+
+=head1 NAME
+
+Koha::ERM::License - Koha ERM License Object class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head2 Internal methods
+
+=head3 _type
+
+=cut
+
+sub _type {
+    return 'ErmLicense';
+}
+
+1;
diff --git a/Koha/ERM/Licenses.pm b/Koha/ERM/Licenses.pm
new file mode 100644 (file)
index 0000000..c45af4d
--- /dev/null
@@ -0,0 +1,53 @@
+package Koha::ERM::Licenses;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+
+use Koha::Database;
+
+use Koha::ERM::License;
+
+use base qw(Koha::Objects);
+
+=head1 NAME
+
+Koha::ERM::Licenses - Koha ERM License Object set class
+
+=head1 API
+
+=head2 Class Methods
+
+=cut
+
+=head3 type
+
+=cut
+
+sub _type {
+    return 'ErmLicense';
+}
+
+=head3 object_class
+
+=cut
+
+sub object_class {
+    return 'Koha::ERM::License';
+}
+
+1;
diff --git a/Koha/REST/V1/ERM/Licenses.pm b/Koha/REST/V1/ERM/Licenses.pm
new file mode 100644 (file)
index 0000000..11573ec
--- /dev/null
@@ -0,0 +1,233 @@
+package Koha::REST::V1::ERM::Licenses;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Mojo::Base 'Mojolicious::Controller';
+
+use Koha::ERM::Licenses;
+
+use Scalar::Util qw( blessed );
+use Try::Tiny qw( catch try );
+
+=head1 API
+
+=head2 Methods
+
+=head3 list
+
+=cut
+
+sub list {
+    my $c = shift->openapi->valid_input or return;
+
+    return try {
+        my $licenses_set = Koha::ERM::Licenses->new;
+        my $licenses = $c->objects->search( $licenses_set );
+        return $c->render( status => 200, openapi => $licenses );
+    }
+    catch {
+        $c->unhandled_exception($_);
+    };
+
+}
+
+=head3 get
+
+Controller function that handles retrieving a single Koha::ERM::License object
+
+=cut
+
+sub get {
+    my $c = shift->openapi->valid_input or return;
+
+    return try {
+        my $license_id = $c->validation->param('license_id');
+        my $license    = $c->objects->find( Koha::ERM::Licenses->search, $license_id );
+
+        unless ($license) {
+            return $c->render(
+                status  => 404,
+                openapi => { error => "License not found" }
+            );
+        }
+
+        return $c->render(
+            status  => 200,
+            openapi => $license
+        );
+    }
+    catch {
+        $c->unhandled_exception($_);
+    };
+}
+
+=head3 add
+
+Controller function that handles adding a new Koha::ERM::License object
+
+=cut
+
+sub add {
+    my $c = shift->openapi->valid_input or return;
+
+    return try {
+        Koha::Database->new->schema->txn_do(
+            sub {
+
+                my $body = $c->validation->param('body');
+
+                my $license = Koha::ERM::License->new_from_api($body)->store;
+
+                $c->res->headers->location($c->req->url->to_string . '/' . $license->license_id);
+                return $c->render(
+                    status  => 201,
+                    openapi => $license->to_api
+                );
+            }
+        );
+    }
+    catch {
+
+        my $to_api_mapping = Koha::ERM::License->new->to_api_mapping;
+
+        if ( blessed $_ ) {
+            if ( $_->isa('Koha::Exceptions::Object::DuplicateID') ) {
+                return $c->render(
+                    status  => 409,
+                    openapi => { error => $_->error, conflict => $_->duplicate_id }
+                );
+            }
+            elsif ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
+                return $c->render(
+                    status  => 400,
+                    openapi => {
+                            error => "Given "
+                            . $to_api_mapping->{ $_->broken_fk }
+                            . " does not exist"
+                    }
+                );
+            }
+            elsif ( $_->isa('Koha::Exceptions::BadParameter') ) {
+                return $c->render(
+                    status  => 400,
+                    openapi => {
+                            error => "Given "
+                            . $to_api_mapping->{ $_->parameter }
+                            . " does not exist"
+                    }
+                );
+            }
+        }
+
+        $c->unhandled_exception($_);
+    };
+}
+
+=head3 update
+
+Controller function that handles updating a Koha::ERM::License object
+
+=cut
+
+sub update {
+    my $c = shift->openapi->valid_input or return;
+
+    my $license_id = $c->validation->param('license_id');
+    my $license = Koha::ERM::Licenses->find( $license_id );
+
+    unless ($license) {
+        return $c->render(
+            status  => 404,
+            openapi => { error => "License not found" }
+        );
+    }
+
+    return try {
+        Koha::Database->new->schema->txn_do(
+            sub {
+
+                my $body = $c->validation->param('body');
+
+                $license->set_from_api($body)->store;
+
+                $c->res->headers->location($c->req->url->to_string . '/' . $license->license_id);
+                return $c->render(
+                    status  => 200,
+                    openapi => $license->to_api
+                );
+            }
+        );
+    }
+    catch {
+        my $to_api_mapping = Koha::ERM::License->new->to_api_mapping;
+
+        if ( blessed $_ ) {
+            if ( $_->isa('Koha::Exceptions::Object::FKConstraint') ) {
+                return $c->render(
+                    status  => 400,
+                    openapi => {
+                            error => "Given "
+                            . $to_api_mapping->{ $_->broken_fk }
+                            . " does not exist"
+                    }
+                );
+            }
+            elsif ( $_->isa('Koha::Exceptions::BadParameter') ) {
+                return $c->render(
+                    status  => 400,
+                    openapi => {
+                            error => "Given "
+                            . $to_api_mapping->{ $_->parameter }
+                            . " does not exist"
+                    }
+                );
+            }
+        }
+
+        $c->unhandled_exception($_);
+    };
+};
+
+=head3 delete
+
+=cut
+
+sub delete {
+    my $c = shift->openapi->valid_input or return;
+
+    my $license = Koha::ERM::Licenses->find( $c->validation->param('license_id') );
+    unless ($license) {
+        return $c->render(
+            status  => 404,
+            openapi => { error => "License not found" }
+        );
+    }
+
+    return try {
+        $license->delete;
+        return $c->render(
+            status  => 204,
+            openapi => q{}
+        );
+    }
+    catch {
+        $c->unhandled_exception($_);
+    };
+}
+
+1;
diff --git a/api/v1/swagger/definitions/erm_license.yaml b/api/v1/swagger/definitions/erm_license.yaml
new file mode 100644 (file)
index 0000000..3431f52
--- /dev/null
@@ -0,0 +1,40 @@
+---
+type: object
+properties:
+  license_id:
+    type: integer
+    description: internally assigned license identifier
+    readOnly: true
+  name:
+    description: name of the license
+    type: string
+  description:
+    description: description of the license
+    type:
+      - string
+      - "null"
+  type:
+    description: description of the license
+    type:
+      - string
+      - "null"
+  status:
+    description: status of the license
+    type: string
+  started_on:
+    type:
+      - string
+      - "null"
+    format: date
+    description: Start of the license
+  ended_on:
+    type:
+      - string
+      - "null"
+    format: date
+    description: End of the license
+additionalProperties: false
+required:
+  - license_id
+  - name
+  - status
diff --git a/api/v1/swagger/definitions/erm_license_agreement.yaml b/api/v1/swagger/definitions/erm_license_agreement.yaml
new file mode 100644 (file)
index 0000000..873bc9b
--- /dev/null
@@ -0,0 +1,34 @@
+---
+type: object
+properties:
+  license_id:
+    type: integer
+    description: internally assigned license identifier
+    readOnly: true
+  agreement_id:
+    description: foreign key to agreements
+    type: integer
+  status:
+    description: status of the license
+    type:
+      - string
+      - "null"
+  physical_location:
+    description: physical location of the license
+    type:
+      - string
+      - "null"
+  notes:
+    description: notes about the license
+    type:
+      - string
+      - "null"
+  uri:
+    description: URI of the license
+    type:
+      - string
+      - "null"
+additionalProperties: false
+required:
+  - license_id
+  - agreement_id
diff --git a/api/v1/swagger/paths/erm_licenses.yaml b/api/v1/swagger/paths/erm_licenses.yaml
new file mode 100644 (file)
index 0000000..d04df9a
--- /dev/null
@@ -0,0 +1,274 @@
+---
+/erm/licenses:
+  get:
+    x-mojo-to: ERM::Licenses#list
+    operationId: listErmLicenses
+    tags:
+      - license
+    summary: List licenses for agreements
+    produces:
+      - application/json
+    parameters:
+      - description: Case insensitive search on license license_id
+        in: query
+        name: license_id
+        required: false
+        type: integer
+      - description: Case insensitive search on license name
+        in: query
+        name: name
+        required: false
+        type: string
+      - description: Case insensitive search on license type
+        in: query
+        name: type
+        required: false
+        type: string
+      - description: Case insensitive search on license status
+        in: query
+        name: status
+        required: false
+        type: string
+      - description: Case insensitive search on license start date
+        in: query
+        name: started_on
+        required: false
+        type: string
+      - description: Case insensitive search on license end date
+        in: query
+        name: ended_on
+        required: false
+        type: string
+      - $ref: "../swagger.yaml#/parameters/match"
+      - $ref: "../swagger.yaml#/parameters/order_by"
+      - $ref: "../swagger.yaml#/parameters/page"
+      - $ref: "../swagger.yaml#/parameters/per_page"
+      - $ref: "../swagger.yaml#/parameters/q_param"
+      - $ref: "../swagger.yaml#/parameters/q_body"
+      - $ref: "../swagger.yaml#/parameters/q_header"
+    responses:
+      200:
+        description: A list of agreements' licenses
+        schema:
+          items:
+            $ref: "../swagger.yaml#/definitions/erm_license"
+          type: array
+      403:
+        description: Access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      500:
+        description: |-
+          Internal server error. Possible `error_code` attribute values:
+          * `internal_server_error`
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      503:
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        erm: 1
+  post:
+    x-mojo-to: ERM::Licenses#add
+    operationId: addERMLicenses
+    tags:
+      - license
+    summary: Add license
+    consumes:
+      - application/json
+    produces:
+      - application/json
+    parameters:
+      - description: A JSON object containing information about the new agreement's license
+        in: body
+        name: body
+        required: true
+        schema:
+            $ref: "../swagger.yaml#/definitions/erm_license"
+    responses:
+      201:
+        description: A successfully created license
+        schema:
+          items:
+            $ref: "../swagger.yaml#/definitions/erm_license"
+      400:
+        description: Bad parameter
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      401:
+        description: Authentication required
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      403:
+        description: Access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      404:
+        description: Ressource not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      409:
+        description: Conflict in creating resource
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      500:
+        description: |-
+          Internal server error. Possible `error_code` attribute values:
+          * `internal_server_error`
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      503:
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        erm: 1
+"/erm/licenses/{license_id}":
+  get:
+    x-mojo-to: ERM::Licenses#get
+    operationId: getERMlicense
+    tags:
+      - license
+    summary: get license
+    produces:
+      - application/json
+    parameters:
+      - $ref: "../swagger.yaml#/parameters/license_id_pp"
+    responses:
+      200:
+        description: license
+        schema:
+          items:
+            $ref: "../swagger.yaml#/definitions/erm_license"
+      401:
+        description: authentication required
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      403:
+        description: access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      404:
+        description: ressource not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      500:
+        description: |-
+          internal server error. possible `error_code` attribute values:
+          * `internal_server_error`
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      503:
+        description: under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        erm: 1
+    x-koha-embed:
+      - agreements
+  put:
+    x-mojo-to: ERM::Licenses#update
+    operationId: updateERMlicenses
+    tags:
+      - license
+    summary: update license
+    consumes:
+      - application/json
+    produces:
+      - application/json
+    parameters:
+      - $ref: "../swagger.yaml#/parameters/license_id_pp"
+      - name: body
+        in: body
+        description: a json object containing new information about existing license
+        required: true
+        schema:
+          $ref: "../swagger.yaml#/definitions/erm_license"
+
+    responses:
+      200:
+        description: a successfully updated license
+        schema:
+          items:
+            $ref: "../swagger.yaml#/definitions/erm_license"
+      400:
+        description: bad parameter
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      403:
+        description: access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      404:
+        description: ressource not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      409:
+        description: conflict in updating resource
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      500:
+        description: |-
+          internal server error. possible `error_code` attribute values:
+          * `internal_server_error`
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      503:
+        description: under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        erm: 1
+    x-koha-embed:
+      - agreements
+  delete:
+    x-mojo-to: ERM::Licenses#delete
+    operationId: deleteERMlicenses
+    tags:
+      - license
+    summary: Delete license
+    produces:
+      - application/json
+    parameters:
+      - $ref: "../swagger.yaml#/parameters/license_id_pp"
+    responses:
+      204:
+        description: license deleted
+      400:
+        description: license deletion failed
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      401:
+        description: authentication required
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      403:
+        description: access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      404:
+        description: ressource not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      409:
+        description: conflict in deleting resource
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      500:
+        description: |-
+          internal server error. possible `error_code` attribute values:
+          * `internal_server_error`
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      503:
+        description: under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+    x-koha-authorization:
+      permissions:
+        erm: 1
index 94b0e0a..57e69db 100644 (file)
@@ -24,6 +24,8 @@ definitions:
     $ref: ./definitions/city.yaml
   erm_agreement:
     $ref: ./definitions/erm_agreement.yaml
+  erm_license:
+    $ref: ./definitions/erm_license.yaml
   error:
     $ref: ./definitions/error.yaml
   fund:
@@ -163,6 +165,10 @@ paths:
     $ref: ./paths/erm_agreements.yaml#/~1erm~1agreements
   "/erm/agreements/{agreement_id}":
     $ref: "./paths/erm_agreements.yaml#/~1erm~1agreements~1{agreement_id}"
+  /erm/licenses:
+    $ref: ./paths/erm_licenses.yaml#/~1erm~1licenses
+  "/erm/licenses/{license_id}":
+    $ref: "./paths/erm_licenses.yaml#/~1erm~1licenses~1{license_id}"
   /erm/users:
     $ref: ./paths/erm_users.yaml#/~1erm~1users
   /holds:
@@ -374,6 +380,12 @@ parameters:
     name: library_id
     required: true
     type: string
+  license_id_pp:
+    description: License internal identifier
+    in: path
+    name: license_id
+    required: true
+    type: integer
   match:
     description: Matching criteria
     enum:
diff --git a/cypress/integration/Licenses_spec.ts b/cypress/integration/Licenses_spec.ts
new file mode 100644 (file)
index 0000000..068fafe
--- /dev/null
@@ -0,0 +1,245 @@
+import { mount } from "@cypress/vue";
+const dayjs = require("dayjs"); /* Cannot use our calendar JS code, it's in an include file (!)
+                                   Also note that moment.js is deprecated */
+
+const dates = {
+    today_iso: dayjs().format("YYYY-MM-DD"),
+    today_us: dayjs().format("MM/DD/YYYY"),
+    tomorrow_iso: dayjs().add(1, "day").format("YYYY-MM-DD"),
+    tomorrow_us: dayjs().add(1, "day").format("MM/DD/YYYY"),
+};
+function get_license() {
+    return {
+        license_id: 1,
+        name: "license 1",
+        description: "my first license",
+        type: "local",
+        status: "active",
+        started_on: dates["today_iso"],
+        ended_on: dates["tomorrow_iso"],
+    };
+}
+
+describe("License CRUD operations", () => {
+    beforeEach(() => {
+        cy.login("koha", "koha");
+        cy.title().should("eq", "Koha staff interface");
+    });
+
+    it("List license", () => {
+        // GET license returns 500
+        cy.intercept("GET", "/api/v1/erm/licenses", {
+            statusCode: 500,
+            error: "Something went wrong",
+        });
+        cy.visit("/cgi-bin/koha/erm/erm.pl");
+        cy.get("#navmenulist").contains("Licenses").click();
+        cy.get("main div[class='dialog alert']").contains(
+            /Something went wrong/
+        );
+
+        // GET licenses returns empty list
+        cy.intercept("GET", "/api/v1/erm/licenses*", []);
+        cy.visit("/cgi-bin/koha/erm/licenses");
+        cy.get("#licenses_list").contains("There are no licenses defined.");
+
+        // GET licenses returns something
+        let license = get_license();
+        let licenses = [license];
+
+        cy.intercept("GET", "/api/v1/erm/licenses*", {
+            statusCode: 200,
+            body: licenses,
+            headers: {
+                "X-Base-Total-Count": "1",
+                "X-Total-Count": "1",
+            },
+        });
+        cy.intercept("GET", "/api/v1/erm/licenses/*", license);
+        cy.visit("/cgi-bin/koha/erm/licenses");
+        cy.get("#licenses_list").contains("Showing 1 to 1 of 1 entries");
+    });
+
+    it("Add license", () => {
+        // Click the button in the toolbar
+        cy.visit("/cgi-bin/koha/erm/licenses");
+        cy.contains("New license").click();
+        cy.get("#licenses_add h2").contains("New license");
+
+        // Fill in the form for normal attributes
+        let license = get_license();
+
+        cy.get("#licenses_add").contains("Submit").click();
+        cy.get("input:invalid,textarea:invalid,select:invalid").should(
+            "have.length",
+            4
+        );
+        cy.get("#license_name").type(license.name);
+        cy.get("#license_description").type(license.description);
+        cy.get("#licenses_add").contains("Submit").click();
+        cy.get("#license_type").select(license.type);
+        cy.get("#license_status").select(license.status);
+
+        cy.get("#started_on").click();
+        cy.get(".flatpickr-calendar")
+            .eq(0)
+            .find("span.today")
+            .click({ force: true });
+
+        cy.get("#ended_on").click();
+        cy.get(".flatpickr-calendar")
+            .eq(1)
+            .find("span.today")
+            .next("span")
+            .click();
+
+        // Submit the form, get 500
+        cy.intercept("POST", "/api/v1/erm/licenses", {
+            statusCode: 500,
+            error: "Something went wrong",
+        });
+        cy.get("#licenses_add").contains("Submit").click();
+        cy.get("main div[class='dialog alert']").contains(
+            "Something went wrong: Internal Server Error"
+        );
+
+        // Submit the form, success!
+        cy.intercept("POST", "/api/v1/erm/licenses", {
+            statusCode: 201,
+            body: license,
+        });
+        cy.get("#licenses_add").contains("Submit").click();
+        cy.get("main div[class='dialog message']").contains(
+            "License created"
+        );
+    });
+
+    it("Edit license", () => {
+        let license = get_license();
+        let licenses = [license];
+        // Click the 'Edit' button from the list
+        cy.intercept("GET", "/api/v1/erm/licenses*", {
+            statusCode: 200,
+            body: licenses,
+            headers: {
+                "X-Base-Total-Count": "1",
+                "X-Total-Count": "1",
+            },
+        });
+        cy.intercept("GET", "/api/v1/erm/licenses/*", license).as(
+            "get-license"
+        );
+        cy.visit("/cgi-bin/koha/erm/licenses");
+        cy.get("#licenses_list table tbody tr:first")
+            .contains("Edit")
+            .click();
+        cy.wait("@get-license");
+        cy.wait(500); // Cypress is too fast! Vue hasn't populated the form yet!
+        cy.get("#licenses_add h2").contains("Edit license");
+
+        // Form has been correctly filled in
+        cy.get("#license_name").should("have.value", license.name);
+        cy.get("#license_description").should(
+            "have.value",
+            license.description
+        );
+        cy.get("#license_type").should("have.value", license.type);
+        cy.get("#license_status").should("have.value", license.status);
+        cy.get("#started_on").invoke("val").should("eq", dates["today_us"]);
+        cy.get("#ended_on").invoke("val").should("eq", dates["tomorrow_us"]);
+
+        // Submit the form, get 500
+        cy.intercept("PUT", "/api/v1/erm/licenses/*", {
+            statusCode: 500,
+            error: "Something went wrong",
+        });
+        cy.get("#licenses_add").contains("Submit").click();
+        cy.get("main div[class='dialog alert']").contains(
+            "Something went wrong: Internal Server Error"
+        );
+
+        // Submit the form, success!
+        cy.intercept("PUT", "/api/v1/erm/licenses/*", {
+            statusCode: 200,
+            body: license,
+        });
+        cy.get("#licenses_add").contains("Submit").click();
+        cy.get("main div[class='dialog message']").contains(
+            "License updated"
+        );
+    });
+
+    it("Show license", () => {
+        let license = get_license();
+        let licenses = [license];
+        // Click the "name" link from the list
+        cy.intercept("GET", "/api/v1/erm/licenses*", {
+            statusCode: 200,
+            body: licenses,
+            headers: {
+                "X-Base-Total-Count": "1",
+                "X-Total-Count": "1",
+            },
+        });
+        cy.intercept("GET", "/api/v1/erm/licenses/*", license).as(
+            "get-license"
+        );
+        cy.visit("/cgi-bin/koha/erm/licenses");
+        let name_link = cy.get(
+            "#licenses_list table tbody tr:first td:first a"
+        );
+        name_link.should(
+            "have.text",
+            license.name + " (#" + license.license_id + ")"
+        );
+        name_link.click();
+        cy.wait("@get-license");
+        cy.wait(500); // Cypress is too fast! Vue hasn't populated the form yet!
+        cy.get("#licenses_show h2").contains(
+            "License #" + license.license_id
+        );
+    });
+
+    it("Delete license", () => {
+        let license = get_license();
+        let licenses = [license];
+
+        // Click the 'Delete' button from the list
+        cy.intercept("GET", "/api/v1/erm/licenses*", {
+            statusCode: 200,
+            body: licenses,
+            headers: {
+                "X-Base-Total-Count": "1",
+                "X-Total-Count": "1",
+            },
+        });
+        cy.intercept("GET", "/api/v1/erm/licenses/*", license);
+        cy.visit("/cgi-bin/koha/erm/licenses");
+
+        cy.get("#licenses_list table tbody tr:first")
+            .contains("Delete")
+            .click();
+        cy.get("#licenses_confirm_delete h2").contains("Delete license");
+        cy.contains("License name: " + license.name);
+
+        // Submit the form, get 500
+        cy.intercept("DELETE", "/api/v1/erm/licenses/*", {
+            statusCode: 500,
+            error: "Something went wrong",
+        });
+        cy.contains("Yes, delete").click();
+        cy.get("main div[class='dialog alert']").contains(
+            "Something went wrong: Internal Server Error"
+        );
+
+        // Submit the form, success!
+        cy.intercept("DELETE", "/api/v1/erm/licenses/*", {
+            statusCode: 204,
+            body: null,
+        });
+        cy.contains("Yes, delete").click();
+        cy.get("main div[class='dialog message']").contains(
+            "License deleted"
+        );
+    });
+});
index ed9cdd5..c6ff7eb 100644 (file)
@@ -97,6 +97,15 @@ VALUES
     ('ERM_AGREEMENT_RENEWAL_PRIORITY', 'cancel', 'Cancel'),
     ('ERM_AGREEMENT_USER_ROLES', 'librarian', 'ERM librarian'),
     ('ERM_AGREEMENT_USER_ROLES', 'subject_specialist', 'Subject specialist'),
+    ('ERM_LICENSE_TYPE', 'local', 'Local'),
+    ('ERM_LICENSE_TYPE', 'consortial', 'Consortial'),
+    ('ERM_LICENSE_TYPE', 'national', 'National'),
+    ('ERM_LICENSE_TYPE', 'alliance', 'Alliance'),
+    ('ERM_LICENSE_STATUS', 'in_negotiation', 'In negociation'),
+    ('ERM_LICENSE_STATUS', 'not_yet_active', 'Not yet active'),
+    ('ERM_LICENSE_STATUS', 'active', 'Active'),
+    ('ERM_LICENSE_STATUS', 'rejected', 'Rejected'),
+    ('ERM_LICENSE_STATUS', 'expired', 'Expired'),
     ('ERM_AGREEMENT_LICENSE_STATUS', 'controlling', 'Controlling'),
     ('ERM_AGREEMENT_LICENSE_STATUS', 'future', 'Future'),
     ('ERM_AGREEMENT_LICENSE_STATUS', 'history', 'Historic'),
index bd71761..2960f8d 100644 (file)
@@ -34,8 +34,8 @@
         const agreement_user_roles = [% To.json(AuthorisedValues.Get('ERM_AGREEMENT_USER_ROLES')) | $raw %];
 
         var table_settings = [% TablesSettings.GetTableSettings( 'erm', 'agreements', 'agreements', 'json' ) | $raw %];
-        var agreements_table_url = '/api/v1/erm/agreements?';
 
+        var agreements_table_url = '/api/v1/erm/agreements?';
         [% IF agreement_name_filter %]
             var agreement_name_filter = {
                 'name': {
             };
             agreements_table_url += 'q='+ encodeURIComponent(JSON.stringify(agreement_name_filter));
         [% END %]
+
+        const license_types = [% To.json(AuthorisedValues.Get('ERM_LICENSE_TYPE')) | $raw %];
+        const license_statuses = [% To.json(AuthorisedValues.Get('ERM_LICENSE_STATUS')) | $raw %];
+
+        var table_settings = [% TablesSettings.GetTableSettings( 'erm', 'agreements', 'agreements', 'json' ) | $raw %];
+
+        var licenses_table_url = '/api/v1/erm/licenses?';
+        [% IF license_name_filter %]
+            var license_name_filter = {
+                'name': {
+                    "like": '%[%- license_name_filter | html -%]%'
+                }
+            };
+            licenses_table_url += 'q='+ encodeURIComponent(JSON.stringify(license_name_filter));
+        [% END %]
+
     </script>
 
     [% Asset.js("js/vue/dist/main.js") | $raw %]
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonDelete.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonDelete.vue
deleted file mode 100644 (file)
index c03df66..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<template>
-    <a class="btn btn-default btn-xs" role="button"
-        ><i class="fa fa-trash" aria-hidden="true" /> Delete</a
-    >
-</template>
-
-<script>
-export default {
-    name: "AgreementsButtonDelete"
-}
-</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonEdit.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsButtonEdit.vue
deleted file mode 100644 (file)
index 1606770..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<template>
-    <a class="btn btn-default btn-xs" role="button"
-        ><i class="fa fa-pencil" aria-hidden="true" /> Edit</a
-    >
-</template>
-
-<script>
-export default {
-    name: "AgreementsButtonEdit",
-}
-</script>
index f4f5c88..1ceb85b 100644 (file)
@@ -1,6 +1,6 @@
 <template>
     <div>
-        <table v-if="agreements.length" id="my_table"></table>
+        <table v-if="agreements.length" id="agreement_list"></table>
         <div v-else-if="this.initialized" class="dialog message">
             There are no agreements defined.
         </div>
@@ -9,8 +9,8 @@
 </template>
 
 <script>
-import AgreementsButtonEdit from "./AgreementsButtonEdit.vue"
-import AgreementsButtonDelete from "./AgreementsButtonDelete.vue"
+import ButtonEdit from "./ButtonEdit.vue"
+import ButtonDelete from "./ButtonDelete.vue"
 import { createVNode, defineComponent, render, resolveComponent } from 'vue'
 export default {
     created() {
@@ -33,7 +33,7 @@ export default {
         let show_agreement = this.show_agreement
         let edit_agreement = this.edit_agreement
         let delete_agreement = this.delete_agreement
-        window['av_vendors'] = this.vendors.map(e => {
+        window['agreements_av_vendors'] = this.vendors.map(e => {
             e['_id'] = e['id']
             e['_str'] = e['name']
             return e
@@ -42,7 +42,7 @@ export default {
             map[e.id] = e
             return map
         }, {})
-        window['av_statuses'] = this.av_statuses.map(e => {
+        window['agreements_av_statuses'] = this.av_statuses.map(e => {
             e['_id'] = e['authorised_value']
             e['_str'] = e['lib']
             return e
@@ -51,7 +51,7 @@ export default {
             map[e.authorised_value] = e
             return map
         }, {})
-        window['av_closure_reasons'] = this.av_closure_reasons.map(e => {
+        window['agreements_av_closure_reasons'] = this.av_closure_reasons.map(e => {
             e['_id'] = e['authorised_value']
             e['_str'] = e['lib']
             return e
@@ -60,7 +60,7 @@ export default {
             map[e.authorised_value] = e
             return map
         }, {})
-        window['av_renewal_priorities'] = this.av_renewal_priorities.map(e => {
+        window['agreements_av_renewal_priorities'] = this.av_renewal_priorities.map(e => {
             e['_id'] = e['authorised_value']
             e['_str'] = e['lib']
             return e
@@ -69,9 +69,9 @@ export default {
             map[e.authorised_value] = e
             return map
         }, {})
-        window['av_is_perpetual'] = [{ _id: 0, _str: _('No') }, { _id: 1, _str: _("Yes") }]
+        window['agreements_av_is_perpetual'] = [{ _id: 0, _str: _('No') }, { _id: 1, _str: _("Yes") }]
 
-        $('#my_table').kohaTable({
+        $('#agreement_list').kohaTable({
             "ajax": {
                 "url": agreements_table_url,
             },
@@ -103,7 +103,7 @@ export default {
                     }
                 },
                 {
-                    "title": __("description"),
+                    "title": __("Description"),
                     "data": "description",
                     "searchable": true,
                     "orderable": true
@@ -160,12 +160,12 @@ export default {
 
                 $.each($(this).find("td .actions"), function (index, e) {
                     let agreement_id = api.row(index).data().agreement_id
-                    let editButton = createVNode(AgreementsButtonEdit, {
+                    let editButton = createVNode(ButtonEdit, {
                         onClick: () => {
                             edit_agreement(agreement_id)
                         }
                     })
-                    let deleteButton = createVNode(AgreementsButtonDelete, {
+                    let deleteButton = createVNode(ButtonDelete, {
                         onClick: () => {
                             delete_agreement(agreement_id)
                         }
@@ -189,17 +189,17 @@ export default {
             },
             preDrawCallback: function (settings) {
                 var table_id = settings.nTable.id
-                $("#" + table_id).find("thead th").eq(1).attr('data-filter', 'av_vendors')
-                $("#" + table_id).find("thead th").eq(3).attr('data-filter', 'av_statuses')
-                $("#" + table_id).find("thead th").eq(4).attr('data-filter', 'av_closure_reasons')
-                $("#" + table_id).find("thead th").eq(5).attr('data-filter', 'av_is_perpetual')
-                $("#" + table_id).find("thead th").eq(6).attr('data-filter', 'av_renewal_priorities')
+                $("#" + table_id).find("thead th").eq(1).attr('data-filter', 'agreements_av_vendors')
+                $("#" + table_id).find("thead th").eq(3).attr('data-filter', 'agreements_av_statuses')
+                $("#" + table_id).find("thead th").eq(4).attr('data-filter', 'agreements_av_closure_reasons')
+                $("#" + table_id).find("thead th").eq(5).attr('data-filter', 'agreements_av_is_perpetual')
+                $("#" + table_id).find("thead th").eq(6).attr('data-filter', 'agreements_av_renewal_priorities')
             }
 
         }, table_settings, 1)
     },
     beforeUnmount() {
-        $('#my_table')
+        $('#agreement_list')
             .DataTable()
             .destroy(true)
     },
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/ButtonDelete.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/ButtonDelete.vue
new file mode 100644 (file)
index 0000000..f435a6b
--- /dev/null
@@ -0,0 +1,11 @@
+<template>
+    <a class="btn btn-default btn-xs" role="button"
+        ><i class="fa fa-trash" aria-hidden="true" /> Delete</a
+    >
+</template>
+
+<script>
+export default {
+    name: "ButtonDelete"
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/ButtonEdit.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/ButtonEdit.vue
new file mode 100644 (file)
index 0000000..d49479f
--- /dev/null
@@ -0,0 +1,11 @@
+<template>
+    <a class="btn btn-default btn-xs" role="button"
+        ><i class="fa fa-pencil" aria-hidden="true" /> Edit</a
+    >
+</template>
+
+<script>
+export default {
+    name: "ButtonEdit",
+}
+</script>
index f65874b..e1245bc 100644 (file)
                                         Agreements</router-link
                                     >
                                 </li>
+                                <li>
+                                    <router-link
+                                        to="/cgi-bin/koha/erm/licenses"
+                                    >
+                                        <i class="fa fa-file-text-o"></i>
+                                        Licenses</router-link
+                                    >
+                                </li>
+
                             </ul>
                         </div>
                     </div>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/Licenses.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/Licenses.vue
new file mode 100644 (file)
index 0000000..72a37a4
--- /dev/null
@@ -0,0 +1,102 @@
+<template>
+    <Toolbar v-if="op == 'list'" @switch-view="switchView" />
+    <div class="dialog message" v-if="message">{{ message }}</div>
+    <div class="dialog alert" v-if="error">{{ error }}</div>
+    <List
+        v-if="op == 'list'"
+        :av_types="types"
+        :av_statuses="statuses"
+        @set-current-license-id="setCurrentLicenseID"
+        @switch-view="switchView"
+        @set-error="setError"
+    />
+    <Show
+        v-if="op == 'show'"
+        :license_id="license_id"
+        :av_types="types"
+        :av_statuses="statuses"
+        @switch-view="switchView"
+        @set-error="setError"
+    />
+    <AddForm
+        v-if="op == 'add-form'"
+        :license_id="license_id"
+        :av_types="types"
+        :av_statuses="statuses"
+        @license-created="licenseCreated"
+        @license-updated="licenseUpdated"
+        @switch-view="switchView"
+        @set-error="setError"
+    />
+    <ConfirmDeleteForm
+        v-if="op == 'confirm-delete-form'"
+        :license_id="license_id"
+        @license-deleted="licenseDeleted"
+        @switch-view="switchView"
+        @set-error="setError"
+    />
+</template>
+
+<script>
+import Toolbar from "./LicensesToolbar.vue"
+import List from "./LicensesList.vue"
+import Show from "./LicensesShow.vue"
+import AddForm from "./LicensesFormAdd.vue"
+import ConfirmDeleteForm from "./LicensesFormConfirmDelete.vue"
+
+import { reactive, computed } from "vue"
+
+export default {
+    data() {
+        return {
+            license_id: null,
+            op: "list",
+            message: null,
+            error: null,
+            types: license_types,
+            statuses: license_statuses,
+        }
+    },
+    methods: {
+        switchView(view) {
+            this.message = null
+            this.error = null
+            this.op = view
+            if (view == "list") this.license_id = null
+        },
+        licenseCreated() {
+            this.message = "License created"
+            this.error = null
+            this.license_id = null
+            this.op = "list"
+        },
+        licenseUpdated() {
+            this.message = "License updated"
+            this.error = null
+            this.license_id = null
+            this.op = "list"
+        },
+        licenseDeleted() {
+            this.message = "License deleted"
+            this.error = null
+            this.license_id = null
+            this.op = "list"
+        },
+        setCurrentLicenseID(license_id) {
+            this.license_id = license_id
+        },
+        setError(error) {
+            this.message = null
+            this.error = "Something went wrong: " + error
+        },
+    },
+    components: {
+        Toolbar,
+        List,
+        Show,
+        AddForm,
+        ConfirmDeleteForm,
+    },
+    emits: ["set-error"],
+};
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesFormAdd.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesFormAdd.vue
new file mode 100644 (file)
index 0000000..736c96b
--- /dev/null
@@ -0,0 +1,205 @@
+<template>
+    <h2 v-if="license.license_id">Edit license</h2>
+    <h2 v-else>New license</h2>
+    <div>
+        <form @submit="onSubmit($event)">
+            <fieldset class="rows">
+                <ol>
+                    <li>
+                        <label class="required" for="license_name"
+                            >License name:</label
+                        >
+                        <input
+                            id="license_name"
+                            v-model="license.name"
+                            placeholder="License name"
+                            required
+                        />
+                        <span class="required">Required</span>
+                    </li>
+                    <li>
+                        <label for="license_description">Description: </label>
+                        <textarea
+                            id="license_description"
+                            v-model="license.description"
+                            placeholder="Description"
+                            rows="10"
+                            cols="50"
+                            required
+                        />
+                        <span class="required">Required</span>
+                    </li>
+                    <li>
+                        <label for="license_type">Type: </label>
+                        <select
+                            id="license_type"
+                            v-model="license.type"
+                            required
+                        >
+                            <option value=""></option>
+                            <option
+                                v-for="type in av_types"
+                                :key="type.authorised_values"
+                                :value="type.authorised_value"
+                                :selected="
+                                    type.authorised_value == license.type
+                                        ? true
+                                        : false
+                                "
+                            >
+                                {{ type.lib }}
+                            </option>
+                        </select>
+                        <span class="required">Required</span>
+                    </li>
+                    <li>
+                        <label for="license_status">Status: </label>
+                        <select
+                            id="license_status"
+                            v-model="license.status"
+                            required
+                        >
+                            <option value=""></option>
+                            <option
+                                v-for="status in av_statuses"
+                                :key="status.authorised_values"
+                                :value="status.authorised_value"
+                                :selected="
+                                    status.authorised_value == license.status
+                                        ? true
+                                        : false
+                                "
+                            >
+                                {{ status.lib }}
+                            </option>
+                        </select>
+                        <span class="required">Required</span>
+                    </li>
+                    <li>
+                        <label for="started_on">Start date: </label>
+                        <flat-pickr
+                            id="started_on"
+                            v-model="license.started_on"
+                            :config="fp_config"
+                            data-date_to="ended_on"
+                        />
+                    </li>
+                    <li>
+                        <label for="ended_on">End date: </label>
+                        <flat-pickr
+                            id="ended_on"
+                            v-model="license.ended_on"
+                            :config="fp_config"
+                        />
+                    </li>
+                </ol>
+            </fieldset>
+            <fieldset class="action">
+                <input type="submit" value="Submit" />
+                <a
+                    role="button"
+                    class="cancel"
+                    @click="$emit('switch-view', 'list')"
+                    >Cancel</a
+                >
+            </fieldset>
+        </form>
+    </div>
+</template>
+
+<script>
+import flatPickr from 'vue-flatpickr-component'
+
+export default {
+    data() {
+        return {
+            fp_config: flatpickr_defaults, dates_fixed: 0,
+
+            license: {
+                license_id: null,
+                name: '',
+                description: '',
+                type: '',
+                status: '',
+                started_on: undefined,
+                ended_on: undefined,
+            }
+        }
+    },
+    beforeUpdate() {
+        if (!this.dates_fixed) {
+            this.license.started_on = $date(this.license.started_on)
+            this.license.ended_on = $date(this.license.ended_on)
+            this.dates_fixed = 1
+        }
+    },
+    created() {
+        if (!this.license_id) return
+        const apiUrl = '/api/v1/erm/licenses/' + this.license_id
+
+        fetch(apiUrl, {
+            //headers: {
+            //    'x-koha-embed': 'periods,user_roles,user_roles.patron'
+            //}
+        })
+            .then(res => res.json())
+            .then(
+                (result) => {
+                    this.license = result
+                },
+                (error) => {
+                    this.$emit('set-error', error)
+                }
+            )
+    },
+    methods: {
+        onSubmit(e) {
+            e.preventDefault()
+
+            let license = JSON.parse(JSON.stringify(this.license)) // copy
+            let apiUrl = '/api/v1/erm/licenses'
+
+            let method = 'POST'
+            if (license.license_id) {
+                method = 'PUT'
+                apiUrl += '/' + license.license_id
+            }
+            delete license.license_id
+
+            license.started_on = license.started_on ? $date_to_rfc3339(license.started_on) : null
+            license.ended_on = license.ended_on ? $date_to_rfc3339(license.ended_on) : null
+
+            const options = {
+                method: method,
+                body: JSON.stringify(license),
+                headers: {
+                    'Content-Type': 'application/json;charset=utf-8'
+                },
+            }
+
+            fetch(apiUrl, options)
+                .then(response => {
+                    if (response.status == 200) {
+                        this.$emit('license-updated')
+                    } else if (response.status == 201) {
+                        this.$emit('license-created')
+                    } else {
+                        this.$emit('set-error', response.message || response.statusText)
+                    }
+                }, (error) => {
+                    this.$emit('set-error', error)
+                }).catch(e => { console.log(e) })
+        },
+    },
+    emits: ['license-created', 'license-updated', 'set-error', 'switch-view'],
+    props: {
+        license_id: Number,
+        av_types: Array,
+        av_statuses: Array,
+    },
+    components: {
+        flatPickr
+    },
+    name: "LicensesFormAdd",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesFormConfirmDelete.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesFormConfirmDelete.vue
new file mode 100644 (file)
index 0000000..e3f4a7b
--- /dev/null
@@ -0,0 +1,85 @@
+<template>
+    <h2>Delete license</h2>
+    <div>
+        <form @submit="onSubmit($event)">
+            <fieldset class="rows">
+                <ol>
+                    <li>
+                        License name:
+                        {{ license.name }}
+                    </li>
+                    <li>
+                        Description:
+                        {{ license.description }}
+                    </li>
+                </ol>
+            </fieldset>
+            <fieldset class="action">
+                <input type="submit" variant="primary" value="Yes, delete" />
+                <a role="button" class="cancel" @click="$emit('switch-view', 'list')"
+                    >No, do not delete</a
+                >
+            </fieldset>
+        </form>
+    </div>
+</template>
+
+<script>
+
+export default {
+    data() {
+        return {
+            license: {},
+        }
+    },
+    created() {
+        const apiUrl = '/api/v1/erm/licenses/' + this.license_id
+
+        fetch(apiUrl)
+            .then(res => res.json())
+            .then(
+                (result) => {
+                    this.license= result
+                },
+            ).catch(
+                (error) => {
+                    this.$emit('set-error', error)
+                }
+            )
+    },
+    methods: {
+        onSubmit(e) {
+            e.preventDefault()
+
+            let apiUrl = '/api/v1/erm/licenses/' + this.license_id
+
+            const options = {
+                method: 'DELETE',
+                headers: {
+                    'Content-Type': 'application/json;charset=utf-8'
+                },
+            }
+
+            fetch(apiUrl, options)
+                .then(
+                    (response) => {
+                        if (response.status == 204) {
+                            this.$emit('license-deleted')
+                        } else {
+                            this.$emit('set-error', response.message || response.statusText)
+                        }
+                    }
+                ).catch(
+                    (error) => {
+                        this.$emit('set-error', error)
+                    }
+                )
+        }
+    },
+    emits: ['license-deleted', 'set-error', 'switch-view'],
+    props: {
+        license_id: Number
+    },
+    name: "LicensesFormConfirmDelete",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesList.vue
new file mode 100644 (file)
index 0000000..a8d284f
--- /dev/null
@@ -0,0 +1,201 @@
+<template>
+    <div>
+        <table v-if="licenses.length" id="license_list"></table>
+        <div v-else-if="this.initialized" class="dialog message">
+            There are no licenses defined.
+        </div>
+        <div v-else>Loading...</div>
+    </div>
+</template>
+
+<script>
+import ButtonEdit from "./ButtonEdit.vue"
+import ButtonDelete from "./ButtonDelete.vue"
+import { createVNode, defineComponent, render, resolveComponent } from 'vue'
+export default {
+    created() {
+        const apiUrl = '/api/v1/erm/licenses'
+
+        fetch(apiUrl)
+            .then(res => res.json())
+            .then(
+                (result) => {
+                    this.licenses = result
+                    this.initialized = true
+                },
+                (error) => {
+                    this.$emit('set-error', error)
+                }
+            )
+    },
+    updated() {
+        let show_license = this.show_license
+        let edit_license = this.edit_license
+        let delete_license = this.delete_license
+        window['licenses_av_types'] = this.av_types.map(e => {
+            e['_id'] = e['authorised_value']
+            e['_str'] = e['lib']
+            return e
+        })
+        let types_map = this.av_types.reduce((map, e) => {
+            map[e.authorised_value] = e
+            return map
+        }, {})
+        window['licenses_av_statuses'] = this.av_statuses.map(e => {
+            e['_id'] = e['authorised_value']
+            e['_str'] = e['lib']
+            return e
+        })
+        let statuses_map = this.av_statuses.reduce((map, e) => {
+            map[e.authorised_value] = e
+            return map
+        }, {})
+
+        $('#license_list').kohaTable({
+            "ajax": {
+                "url": licenses_table_url,
+            },
+            "order": [[1, "asc"]],
+            "columnDefs": [{
+                "targets": [0, 1],
+                "render": function (data, type, row, meta) {
+                    if (type == 'display') {
+                        return escape_str(data)
+                    }
+                    return data
+                }
+            }],
+            "columns": [
+                {
+                    "title": __("Name"),
+                    "data": ["me.license_id", "me.name"],
+                    "searchable": true,
+                    "orderable": true,
+                    // Rendering done in drawCallback
+                },
+                {
+                    "title": __("Description"),
+                    "data": "description",
+                    "searchable": true,
+                    "orderable": true
+                },
+                {
+                    "title": __("Type"),
+                    "data": "type",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return escape_str(types_map[row.type].lib)
+                    }
+                },
+                {
+                    "title": __("Status"),
+                    "data": "status",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return escape_str(statuses_map[row.status].lib)
+                    }
+                },
+                {
+                    "title": __("Started on"),
+                    "data": "started_on",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return escape_str(row.started_on)
+                    }
+                },
+                {
+                    "title": __("Ended on"),
+                    "data": "ended_on",
+                    "searchable": true,
+                    "orderable": true,
+                    "render": function (data, type, row, meta) {
+                        return escape_str(row.ended_on)
+                    }
+                },
+                {
+                    "title": __("Actions"),
+                    "data": function (row, type, val, meta) {
+                        return '<div class="actions"></div>'
+                    },
+                    "className": "actions noExport",
+                    "searchable": false,
+                    "orderable": false
+                }
+            ],
+            drawCallback: function (settings) {
+
+                var api = new $.fn.dataTable.Api(settings)
+
+                $.each($(this).find("td .actions"), function (index, e) {
+                    let license_id = api.row(index).data().license_id
+                    let editButton = createVNode(ButtonEdit, {
+                        onClick: () => {
+                            edit_license(license_id)
+                        }
+                    })
+                    let deleteButton = createVNode(ButtonDelete, {
+                        onClick: () => {
+                            delete_license(license_id)
+                        }
+                    })
+                    let n = createVNode('span', {}, [editButton, " ", deleteButton])
+                    render(n, e)
+                })
+
+                $.each($(this).find("tbody tr td:first-child"), function (index, e) {
+                    let row = api.row(index).data()
+                    if (!row) return // Happen if the table is empty
+                    let n = createVNode("a", {
+                        role: "button",
+                        onClick: () => {
+                            show_license(row.license_id)
+                        }
+                    },
+                        escape_str(`${row.name} (#${row.license_id})`)
+                    )
+                    render(n, e)
+                })
+            },
+            preDrawCallback: function (settings) {
+                var table_id = settings.nTable.id
+                $("#" + table_id).find("thead th").eq(2).attr('data-filter', 'licenses_av_types')
+                $("#" + table_id).find("thead th").eq(3).attr('data-filter', 'licenses_av_statuses')
+            }
+
+        }, table_settings, 1)
+    },
+    beforeUnmount() {
+        $('#license_list')
+            .DataTable()
+            .destroy(true)
+    },
+    data: function () {
+        return {
+            licenses: [],
+            initialized: false,
+        }
+    },
+    methods: {
+        show_license: function (license_id) {
+            this.$emit('set-current-license-id', license_id)
+            this.$emit('switch-view', 'show')
+        },
+        edit_license: function (license_id) {
+            this.$emit('set-current-license-id', license_id)
+            this.$emit('switch-view', 'add-form')
+        },
+        delete_license: function (license_id) {
+            this.$emit('set-current-license-id', license_id)
+            this.$emit('switch-view', 'confirm-delete-form')
+        },
+    },
+    props: {
+        av_types: Array,
+        av_statuses: Array,
+    },
+    name: "LicensesList",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesShow.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesShow.vue
new file mode 100644 (file)
index 0000000..59d6ffb
--- /dev/null
@@ -0,0 +1,114 @@
+<template>
+    <h2>License #{{ license.license_id }}</h2>
+    <div>
+        <fieldset class="rows">
+            <ol>
+                <li>
+                    <label>License name:</label>
+                    <span>
+                        {{ license.name }}
+                    </span>
+                </li>
+                <li>
+                    <label>Description: </label>
+                    <span>
+                        {{ license.description }}
+                    </span>
+                </li>
+                <li>
+                    <label>Type: </label>
+                    <span>{{
+                        get_lib_from_av(av_types, license.type)
+                    }}</span>
+                </li>
+                <li>
+                    <label>Status: </label>
+                    <span>{{
+                        get_lib_from_av(av_statuses, license.status)
+                    }}</span>
+                </li>
+
+                <li>
+                    <label>Started on:</label>
+                    <span>{{ format_date(license.started_on) }}</span>
+                </li>
+
+                <li>
+                    <label>Ended on:</label>
+                    <span>{{ format_date(license.ended_on) }}</span>
+                </li>
+
+            </ol>
+        </fieldset>
+        <fieldset class="action">
+            <a
+                role="button"
+                class="cancel"
+                @click="$emit('switch-view', 'list')"
+                >Close</a
+            >
+        </fieldset>
+    </div>
+</template>
+
+<script>
+
+export default {
+    setup() {
+        const format_date = $date
+        const get_lib_from_av = function (arr, av) {
+            let o = arr.find(
+                (e) => e.authorised_value == av
+            )
+            return o ? o.lib : ""
+        }
+        return {
+            format_date,
+            get_lib_from_av
+        }
+    },
+    data() {
+        return {
+            license: {
+                license_id: null,
+                name: '',
+                description: '',
+                type: '',
+                status: '',
+                started_on: undefined,
+                ended_on: undefined,
+            }
+        }
+    },
+    created() {
+        if (!this.license_id) return
+        const apiUrl = '/api/v1/erm/licenses/' + this.license_id
+
+        fetch(apiUrl, {
+            //headers: {
+            //    'x-koha-embed': 'periods,user_roles,user_roles.patron'
+            //}
+        })
+            .then(res => res.json())
+            .then(
+                (result) => {
+                    this.license = result
+                },
+                (error) => {
+                    this.$emit('set-error', error)
+                }
+            )
+    },
+    methods: {
+    },
+    emits: ['set-error', 'switch-view'],
+    props: {
+        license_id: Number,
+        av_types: Array,
+        av_statuses: Array,
+    },
+    components: {
+    },
+    name: "LicensesShow",
+}
+</script>
diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesToolbar.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesToolbar.vue
new file mode 100644 (file)
index 0000000..c4b634c
--- /dev/null
@@ -0,0 +1,12 @@
+<template>
+    <a class="btn btn-default" @click="$emit('switch-view', 'add-form')"
+        ><font-awesome-icon icon="plus" /> New license</a
+    >
+</template>
+
+<script>
+export default {
+    name: "LicensesToolbar",
+    emits: ['switch-view'],
+}
+</script>
index a38106c..906e178 100644 (file)
@@ -10,6 +10,7 @@ library.add(faPlus, faPencil, faTrash);
 import App from "./components/ERM/ERMMain.vue";
 import ERMHome from "./components/ERM/ERMHome.vue";
 import Agreements from "./components/ERM/Agreements.vue";
+import Licenses from "./components/ERM/Licenses.vue";
 
 const Bar = { template: "<div>bar</div>" };
 const routes = [
@@ -40,6 +41,20 @@ const routes = [
             ],
         },
     },
+    {
+        path: "/cgi-bin/koha/erm/licenses",
+        component: Licenses,
+        meta: {
+            breadcrumb: [
+                { text: "Home", path: "/cgi-bin/koha/mainpage.pl" },
+                {
+                    text: "Electronic resources management",
+                    path: "/cgi-bin/koha/erm/erm.pl",
+                },
+                { text: "Licenses", path: "/cgi-bin/koha/erm/licenses" },
+            ],
+        },
+    },
 ];
 
 const router = createRouter({ history: createWebHistory(), routes });
diff --git a/t/db_dependent/api/v1/erm_licenses.t b/t/db_dependent/api/v1/erm_licenses.t
new file mode 100755 (executable)
index 0000000..14b3552
--- /dev/null
@@ -0,0 +1,353 @@
+#!/usr/bin/env perl
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+use Test::More tests => 5;
+use Test::Mojo;
+
+use t::lib::TestBuilder;
+use t::lib::Mocks;
+
+use Koha::ERM::Licenses;
+use Koha::Database;
+
+my $schema  = Koha::Database->new->schema;
+my $builder = t::lib::TestBuilder->new;
+
+my $t = Test::Mojo->new('Koha::REST::V1');
+
+subtest 'list() tests' => sub {
+
+    plan tests => 8;
+
+    $schema->storage->txn_begin;
+
+    Koha::ERM::Licenses->search->delete;
+
+    my $librarian = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 2**28 }
+        }
+    );
+    my $password = 'thePassword123';
+    $librarian->set_password( { password => $password, skip_validation => 1 } );
+    my $userid = $librarian->userid;
+
+    my $patron = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 0 }
+        }
+    );
+
+    $patron->set_password( { password => $password, skip_validation => 1 } );
+    my $unauth_userid = $patron->userid;
+
+    ## Authorized user tests
+    # No licenses, so empty array should be returned
+    $t->get_ok("//$userid:$password@/api/v1/erm/licenses")->status_is(200)
+      ->json_is( [] );
+
+    my $license =
+      $builder->build_object( { class => 'Koha::ERM::Licenses' } );
+
+    # One license created, should get returned
+    $t->get_ok("//$userid:$password@/api/v1/erm/licenses")->status_is(200)
+      ->json_is( [ $license->to_api ] );
+
+    # Unauthorized access
+    $t->get_ok("//$unauth_userid:$password@/api/v1/erm/licenses")
+      ->status_is(403);
+
+    $schema->storage->txn_rollback;
+};
+
+subtest 'get() tests' => sub {
+
+    plan tests => 8;
+
+    $schema->storage->txn_begin;
+
+    my $license =
+      $builder->build_object( { class => 'Koha::ERM::Licenses' } );
+    my $librarian = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 2**28 }
+        }
+    );
+    my $password = 'thePassword123';
+    $librarian->set_password( { password => $password, skip_validation => 1 } );
+    my $userid = $librarian->userid;
+
+    my $patron = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 0 }
+        }
+    );
+
+    $patron->set_password( { password => $password, skip_validation => 1 } );
+    my $unauth_userid = $patron->userid;
+
+    $t->get_ok( "//$userid:$password@/api/v1/erm/licenses/"
+          . $license->license_id )->status_is(200)
+      ->json_is( $license->to_api );
+
+    $t->get_ok( "//$unauth_userid:$password@/api/v1/erm/licenses/"
+          . $license->license_id )->status_is(403);
+
+    my $license_to_delete =
+      $builder->build_object( { class => 'Koha::ERM::Licenses' } );
+    my $non_existent_id = $license_to_delete->id;
+    $license_to_delete->delete;
+
+    $t->get_ok("//$userid:$password@/api/v1/erm/licenses/$non_existent_id")
+      ->status_is(404)->json_is( '/error' => 'License not found' );
+
+    $schema->storage->txn_rollback;
+};
+
+subtest 'add() tests' => sub {
+
+    plan tests => 18;
+
+    $schema->storage->txn_begin;
+
+    my $librarian = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 2**28 }
+        }
+    );
+    my $password = 'thePassword123';
+    $librarian->set_password( { password => $password, skip_validation => 1 } );
+    my $userid = $librarian->userid;
+
+    my $patron = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 0 }
+        }
+    );
+
+    $patron->set_password( { password => $password, skip_validation => 1 } );
+    my $unauth_userid = $patron->userid;
+
+    my $license = {
+        name             => "License name",
+        description      => "License description",
+        type             => 'local',
+        status           => "active",
+        started_on       => undef,
+        ended_on         => undef,
+    };
+
+    # Unauthorized attempt to write
+    $t->post_ok( "//$unauth_userid:$password@/api/v1/erm/licenses" => json =>
+          $license )->status_is(403);
+
+    # Authorized attempt to write invalid data
+    my $license_with_invalid_field = {
+        blah             => "License Blah",
+        name             => "License name",
+        description      => "License description",
+        type             => 'local',
+        status           => "active",
+        started_on       => undef,
+        ended_on         => undef,
+    };
+
+    $t->post_ok( "//$userid:$password@/api/v1/erm/licenses" => json =>
+          $license_with_invalid_field )->status_is(400)->json_is(
+        "/errors" => [
+            {
+                message => "Properties not allowed: blah.",
+                path    => "/body"
+            }
+        ]
+          );
+
+    # Authorized attempt to write
+    my $license_id =
+      $t->post_ok(
+        "//$userid:$password@/api/v1/erm/licenses" => json => $license )
+      ->status_is( 201, 'SWAGGER3.2.1' )->header_like(
+        Location => qr|^/api/v1/erm/licenses/\d*|,
+        'SWAGGER3.4.1'
+    )->json_is( '/name'             => $license->{name} )
+      ->json_is( '/description'     => $license->{description} )
+      ->json_is( '/type'            => $license->{type} )
+      ->json_is( '/status'          => $license->{status} )
+      ->tx->res->json->{license_id};
+
+    # Authorized attempt to create with null id
+    $license->{license_id} = undef;
+    $t->post_ok(
+        "//$userid:$password@/api/v1/erm/licenses" => json => $license )
+      ->status_is(400)->json_has('/errors');
+
+    # Authorized attempt to create with existing id
+    $license->{license_id} = $license_id;
+    $t->post_ok(
+        "//$userid:$password@/api/v1/erm/licenses" => json => $license )
+      ->status_is(400)->json_is(
+        "/errors" => [
+            {
+                message => "Read-only.",
+                path    => "/body/license_id"
+            }
+        ]
+      );
+
+    $schema->storage->txn_rollback;
+};
+
+subtest 'update() tests' => sub {
+
+    plan tests => 12;
+
+    $schema->storage->txn_begin;
+
+    my $librarian = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 2**28 }
+        }
+    );
+    my $password = 'thePassword123';
+    $librarian->set_password( { password => $password, skip_validation => 1 } );
+    my $userid = $librarian->userid;
+
+    my $patron = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 0 }
+        }
+    );
+
+    $patron->set_password( { password => $password, skip_validation => 1 } );
+    my $unauth_userid = $patron->userid;
+
+    my $license_id =
+      $builder->build_object( { class => 'Koha::ERM::Licenses' } )->license_id;
+
+    # Unauthorized attempt to update
+    $t->put_ok(
+        "//$unauth_userid:$password@/api/v1/erm/licenses/$license_id" =>
+          json => { name => 'New unauthorized name change' } )->status_is(403);
+
+    # Full object update on PUT
+    my $license_with_updated_field = {
+        name             => 'New name',
+        description      => 'New description',
+        type             => 'national',
+        status           => 'expired',
+        started_on       => undef,
+        ended_on         => undef,
+    };
+
+    $t->put_ok(
+        "//$userid:$password@/api/v1/erm/licenses/$license_id" => json =>
+          $license_with_updated_field )->status_is(200)
+      ->json_is( '/name' => 'New name' );
+
+    # Authorized attempt to write invalid data
+    my $license_with_invalid_field = {
+        blah             => "License Blah",
+        name             => "License name",
+        description      => "License description",
+        type             => 'national',
+        status           => 'expired',
+        started_on       => undef,
+        ended_on         => undef,
+    };
+
+    $t->put_ok(
+        "//$userid:$password@/api/v1/erm/licenses/$license_id" => json =>
+          $license_with_invalid_field )->status_is(400)->json_is(
+        "/errors" => [
+            {
+                message => "Properties not allowed: blah.",
+                path    => "/body"
+            }
+        ]
+          );
+
+    my $license_to_delete =
+      $builder->build_object( { class => 'Koha::ERM::Licenses' } );
+    my $non_existent_id = $license_to_delete->id;
+    $license_to_delete->delete;
+
+    $t->put_ok( "//$userid:$password@/api/v1/erm/licenses/$non_existent_id" =>
+          json => $license_with_updated_field )->status_is(404);
+
+    # Wrong method (POST)
+    $license_with_updated_field->{license_id} = 2;
+
+    $t->post_ok(
+        "//$userid:$password@/api/v1/erm/licenses/$license_id" => json =>
+          $license_with_updated_field )->status_is(404);
+
+    $schema->storage->txn_rollback;
+};
+
+subtest 'delete() tests' => sub {
+
+    plan tests => 7;
+
+    $schema->storage->txn_begin;
+
+    my $librarian = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 2**28 }
+        }
+    );
+    my $password = 'thePassword123';
+    $librarian->set_password( { password => $password, skip_validation => 1 } );
+    my $userid = $librarian->userid;
+
+    my $patron = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 0 }
+        }
+    );
+
+    $patron->set_password( { password => $password, skip_validation => 1 } );
+    my $unauth_userid = $patron->userid;
+
+    my $license_id =
+      $builder->build_object( { class => 'Koha::ERM::Licenses' } )->id;
+
+    # Unauthorized attempt to delete
+    $t->delete_ok(
+        "//$unauth_userid:$password@/api/v1/erm/licenses/$license_id")
+      ->status_is(403);
+
+    $t->delete_ok("//$userid:$password@/api/v1/erm/licenses/$license_id")
+      ->status_is( 204, 'SWAGGER3.2.4' )->content_is( '', 'SWAGGER3.3.4' );
+
+    $t->delete_ok("//$userid:$password@/api/v1/erm/licenses/$license_id")
+      ->status_is(404);
+
+    $schema->storage->txn_rollback;
+};
+