<commit_before>130-API-biblios-CRUD-expanded

Patch to enable full CRUD on /api/v1/biblios
Delete all items on DELETE as default, except if items have holds or issues
Also enables expanded item with status

diff --git a/Koha/REST/V1/Biblio.pm b/Koha/REST/V1/Biblio.pm
new file mode 100644
index 0000000..8db7361
--- /dev/null
+++ b/Koha/REST/V1/Biblio.pm
@@ -0,0 +1,176 @@
+package Koha::REST::V1::Biblio;
+
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Mojo::Base 'Mojolicious::Controller';
+
+use Koha::AuthUtils qw(hash_password);
+use C4::Biblio qw( GetBiblioData AddBiblio ModBiblio DelBiblio );
+use C4::Items qw ( AddItemBatchFromMarc );
+use Koha::Biblios;
+use MARC::Record;
+use MARC::Batch;
+use MARC::File::USMARC;
+use MARC::File::XML;
+
+use Data::Dumper;
+
+sub get {
+    my ($c, $args, $cb) = @_;
+
+    my $biblio = &GetBiblioData($args->{biblionumber});
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+    return $c->$cb($biblio, 200);
+}
+
+sub getitems {
+    my ($c, $args, $cb) = @_;
+
+    #my $biblio = &GetBiblioData($args->{biblionumber});
+    my $biblio = Koha::Biblios->find($args->{biblionumber});
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+    return $c->$cb({ biblio => $biblio->unblessed, items => $biblio->items->unblessed }, 200);
+}
+
+sub getexpanded {
+    my ($c, $args, $cb) = @_;
+
+    my $biblio = Koha::Biblios->find($args->{biblionumber});
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+    my $expanded = $biblio->items->unblessed;
+    for my $item (@{$expanded}) {
+
+        # we assume item is available by default
+        $item->{status} = "Ledig";
+
+        if ($item->{onloan}) {
+            $item->{status} = "Utlånt"
+        }
+
+        if ($item->{notforloan}) {
+            $item->{status} = "Ikke til utlån";
+        }
+
+        # mark as unavailable if notforloan, damaged, lost, or withdrawn
+        if ($item->{damaged} || $item->{itemlost} || $item->{withdrawn} || $item->{restricted}) {
+            $item->{status} = "Utilgjengelig";
+        }
+
+        my $holds = Koha::Holds->search({itemnumber => $item->{itemnumber}})->unblessed;
+
+        # mark as checked out if item marked as hold
+        if (scalar(@{$holds}) > 0) {
+            $item->{status} = "Utlånt";
+        }
+    }
+
+    return $c->$cb({ biblio => $biblio->unblessed, items => $expanded }, 200);
+}
+
+sub add {
+    my ($c, $args, $cb) = @_;
+
+    my $biblionumber;
+    my $biblioitemnumber;
+
+    my $body = $c->req->body;
+    unless ($body) {
+        return $c->$cb({error => "Missing MARCXML body"}, 400);
+    }
+
+    my $record = eval {MARC::Record::new_from_xml( $body, "utf8", '')};
+    if ($@) {
+        return $c->$cb({error => $@}, 400);
+    } else {
+        my $framework = $c->req->headers->header('framework');
+        ( $biblionumber, $biblioitemnumber ) = &AddBiblio($record, $framework || '');
+    }
+    if ($biblionumber) {
+        $c->res->headers->location($c->url_for('/api/v1/biblios/')->to_abs . $biblionumber);
+        my ( $itemnumbers, $errors ) = &AddItemBatchFromMarc( $record, $biblionumber, $biblioitemnumber, '' );
+        unless (@{$errors}) {
+            return $c->$cb({biblionumber => $biblionumber, items => join(",", @{$itemnumbers})}, 201);
+        } else {
+            warn Dumper($errors);
+            return $c->$cb({error => "Error creating items, see Koha Logs for details.", biblionumber => $biblionumber, items => join(",", @{$itemnumbers})}, 400);
+        }
+    } else {
+        return $c->$cb({error => "unable to create record"}, 400);
+    }
+}
+
+# This will not update items, Items is a separate API route
+sub update {
+    my ($c, $args, $cb) = @_;
+
+    my $biblionumber = $args->{biblionumber};
+
+    my $biblio = Koha::Biblios->find($biblionumber);
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+
+    my $success;
+    my $body = $c->req->body;
+    my $record = eval {MARC::Record::new_from_xml( $body, "utf8", '')};
+    if ($@) {
+        return $c->$cb({error => $@}, 400);
+    } else {
+        $success = &ModBiblio($record, $biblionumber, '');
+    }
+    if ($success) {
+        $c->res->headers->location($c->url_for('/api/v1/biblios/')->to_abs . $biblionumber);
+        return $c->$cb({biblio => Koha::Biblios->find($biblionumber)->unblessed}, 200);
+    } else {
+        return $c->$cb({error => "unable to update record"}, 400);
+    }
+}
+
+sub delete {
+    my ($c, $args, $cb) = @_;
+
+    my $biblio = Koha::Biblios->find($args->{biblionumber});
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+    my @items = $biblio->items;
+    # Delete items first
+    my @item_errors = ();
+    foreach my $item (@items) {
+        my $res = $item->delete;
+        unless ($res eq 1) {
+            push @item_errors, $item->unblessed->{itemnumber};
+        }
+    }
+    my $res = $biblio->delete;
+    if ($res eq '1') {
+        return $c->$cb({}, 200);
+    } elsif ($res eq '-1') {
+        return $c->$cb({error => "Not found. Error code: " . $res, items => @item_errors}, 404);
+    } else {
+        return $c->$cb({error => "Error code: " . $res, items => @item_errors}, 400);
+    }
+}
+
+1;
diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json
index 0a00e97..d1cd3eb 100644
--- a/api/v1/swagger/definitions.json
+++ b/api/v1/swagger/definitions.json
@@ -1,4 +1,10 @@
 {
+  "biblios": {
+    "$ref": "definitions/biblios.json"
+  },
+  "biblio": {
+    "$ref": "definitions/biblio.json"
+  },
   "items": {
     "$ref": "definitions/items.json"
   },
diff --git a/api/v1/swagger/definitions/biblios.json b/api/v1/swagger/definitions/biblios.json
new file mode 100644
index 0000000..b0600e8
--- /dev/null
+++ b/api/v1/swagger/definitions/biblios.json
@@ -0,0 +1,4 @@
+{
+    "type": "array",
+    "items": { "$ref": "biblio.json" }
+}
diff --git a/api/v1/swagger/definitions/biblio.json b/api/v1/swagger/definitions/biblio.json
new file mode 100644
index 0000000..bde3f8f
--- /dev/null
+++ b/api/v1/swagger/definitions/biblio.json
@@ -0,0 +1,48 @@
+{
+  "type": "object",
+    "properties": {
+    "biblionumber": {
+      "$ref": "../x-primitives.json#/biblionumber"
+    },
+    "author": {
+      "type": ["string", "null"],
+      "description": "statement of responsibility from MARC record (100$a in MARC21)"
+    },
+    "title": {
+      "type": ["string", "null"],
+      "description": "title (without the subtitle) from the MARC record (245$a in MARC21)"
+    },
+    "unititle": {
+      "type": ["string", "null"],
+      "description": "uniform title (without the subtitle) from the MARC record (240$a in MARC21)"
+    },
+    "notes": {
+      "type": ["string", "null"],
+      "description": "values from the general notes field in the MARC record (500$a in MARC21) split by bar (|)"
+    },
+    "serial": {
+      "type": ["string", "null"],
+      "description": "Boolean indicating whether biblio is for a serial"
+    },
+    "seriestitle": {
+      "type": ["string", "null"],
+      "description": ""
+    },
+    "copyrightdate": {
+      "type": ["string", "null"],
+      "description": "publication or copyright date from the MARC record"
+    },
+    "timestamp": {
+      "type": ["string", "null"],
+      "description": "date and time this record was last touched"
+    },
+    "datecreated": {
+      "type": ["string", "null"],
+      "description": "the date this record was added to Koha"
+    },
+    "abstract": {
+      "type": ["string", "null"],
+      "description": "summary from the MARC record (520$a in MARC21)"
+    }
+  }
+}
diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json
index ce79d4c..4ed7de0 100644
--- a/api/v1/swagger/parameters.json
+++ b/api/v1/swagger/parameters.json
@@ -1,4 +1,7 @@
 {
+  "biblionumberPathParam": {
+    "$ref": "parameters/biblio.json#/biblionumberPathParam"
+   },
   "itemnumberPathParam": {
     "$ref": "parameters/item.json#/itemnumberPathParam"
   },
diff --git a/api/v1/swagger/parameters/biblio.json b/api/v1/swagger/parameters/biblio.json
new file mode 100644
index 0000000..1d1316a
--- /dev/null
+++ b/api/v1/swagger/parameters/biblio.json
@@ -0,0 +1,9 @@
+{
+   "biblionumberPathParam": {
+     "name": "biblionumber",
+     "in": "path",
+     "description": "Internal biblio identifier",
+     "required": true,
+     "type": "integer"
+  }
+}
diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json
index 532eb0d..d768999 100644
--- a/api/v1/swagger/paths.json
+++ b/api/v1/swagger/paths.json
@@ -1,4 +1,16 @@
 {
+  "/biblios": {
+    "$ref": "paths/biblios.json#/~1biblios"
+  },
+  "/biblios/{biblionumber}": {
+    "$ref": "paths/biblios.json#/~1biblios~1{biblionumber}"
+  },
+  "/biblios/{biblionumber}/items": {
+    "$ref": "paths/biblios.json#/~1biblios~1{biblionumber}~1items"
+  },
+  "/biblios/{biblionumber}/expanded": {
+    "$ref": "paths/biblios.json#/~1biblios~1{biblionumber}~1expanded"
+  },
   "/items/{itemnumber}": {
     "$ref": "paths/items.json#/~1items~1{itemnumber}"
   },
diff --git a/api/v1/swagger/paths/biblios.json b/api/v1/swagger/paths/biblios.json
new file mode 100644
index 0000000..d58f205
--- /dev/null
+++ b/api/v1/swagger/paths/biblios.json
@@ -0,0 +1,153 @@
+{
+  "/biblios/{biblionumber}": {
+    "get": {
+      "operationId": "getBiblio",
+      "tags": ["biblios"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": [
+        "application/json"
+      ],
+      "responses": {
+        "200": {
+          "description": "A biblio record",
+          "schema": { "$ref": "../definitions.json#/biblio" }
+
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": {
+            "$ref": "../definitions.json#/error"
+          }
+        }
+      }
+    },
+    "put": {
+      "operationId": "updateBiblio",
+      "tags": ["biblios"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": [
+        "application/json"
+      ],
+      "responses": {
+        "200": {
+          "description": "An updated biblio record"
+        },
+        "400": {
+          "description": "Biblio update failed",
+          "schema": { "$ref": "../definitions.json#/error" }
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": {
+            "$ref": "../definitions.json#/error"
+          }
+        }
+      },
+      "x-koha-authorization": {
+        "permissions": {
+          "editcatalogue": "1"
+        }
+      }
+    },
+    "delete": {
+      "operationId": "deleteBiblio",
+      "tags": ["biblios"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": ["application/json"],
+      "responses": {
+        "200": {
+          "description": "Biblio record deleted successfully",
+          "schema": {
+            "type": "object"
+          }
+        },
+        "400": {
+          "description": "Biblio deletion failed",
+          "schema": { "$ref": "../definitions.json#/error" }
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": { "$ref": "../definitions.json#/error" }
+        }
+      },
+      "x-koha-authorization": {
+        "permissions": {
+          "editcatalogue": "1"
+        }
+      }
+    }
+  },
+  "/biblios/{biblionumber}/items": {
+    "get": {
+      "operationId": "getitemsByBiblio",
+      "tags": ["biblios", "items"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": [
+        "application/json"
+      ],
+      "responses": {
+        "200": {
+          "description": "A biblio record with items"
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": {
+            "$ref": "../definitions.json#/error"
+          }
+        }
+      }
+    }
+  },
+  "/biblios/{biblionumber}/expanded": {
+    "get": {
+      "operationId": "getexpandedByBiblio",
+      "tags": ["biblios", "items", "item status"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": [
+        "application/json"
+      ],
+      "responses": {
+        "200": {
+          "description": "A biblio record with items and item statuses"
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": {
+            "$ref": "../definitions.json#/error"
+          }
+        }
+      }
+    }
+  },
+  "/biblios": {
+    "post": {
+      "operationId": "addBiblio",
+      "tags": ["biblios"],
+      "produces": ["application/json"],
+      "responses": {
+        "201": {
+          "description": "A new biblio record"
+        },
+        "400": {
+          "description": "Unable to create biblio record",
+          "schema": { "$ref": "../definitions.json#/error" }
+        }
+      }
+    },
+    "x-koha-authorization": {
+      "permissions": {
+        "editcatalogue": "1"
+      }
+    }
+  }
+}
<commit_msg>Improve item status details DEICH-386<commit_after>130-API-biblios-CRUD-expanded

Patch to enable full CRUD on /api/v1/biblios
Delete all items on DELETE as default, except if items have holds or issues
Also enables expanded item with status

diff --git a/Koha/REST/V1/Biblio.pm b/Koha/REST/V1/Biblio.pm
new file mode 100644
index 0000000..a70efda
--- /dev/null
+++ b/Koha/REST/V1/Biblio.pm
@@ -0,0 +1,201 @@
+package Koha::REST::V1::Biblio;
+
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Mojo::Base 'Mojolicious::Controller';
+
+use Koha::AuthUtils qw(hash_password);
+use C4::Biblio qw( GetBiblioData AddBiblio ModBiblio DelBiblio );
+use C4::Items qw ( AddItemBatchFromMarc );
+use Koha::Biblios;
+use MARC::Record;
+use MARC::Batch;
+use MARC::File::USMARC;
+use MARC::File::XML;
+
+use Data::Dumper;
+
+sub get {
+    my ($c, $args, $cb) = @_;
+
+    my $biblio = &GetBiblioData($args->{biblionumber});
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+    return $c->$cb($biblio, 200);
+}
+
+sub getitems {
+    my ($c, $args, $cb) = @_;
+
+    #my $biblio = &GetBiblioData($args->{biblionumber});
+    my $biblio = Koha::Biblios->find($args->{biblionumber});
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+    return $c->$cb({ biblio => $biblio->unblessed, items => $biblio->items->unblessed }, 200);
+}
+
+sub getexpanded {
+    my ($c, $args, $cb) = @_;
+
+    my $biblio = Koha::Biblios->find($args->{biblionumber});
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+
+    my $expanded = $biblio->items->unblessed;
+    for my $item (@{$expanded}) {
+
+        # we assume item is available and reservable by default
+        $item->{status} = "Ledig";
+        $item->{reservable} = 1;
+
+        if ($item->{holdingbranch} ne $item->{homebranch}) {
+            # This will match items in branchtransfers
+            $item->{status} = "Utlånt";
+        }
+
+        if ($item->{onloan}) {
+            $item->{status} = "Utlånt";
+        }
+
+        my $notloan = $item->{notforloan};
+        if ($notloan) {
+            # -2  Bestilt
+            #  5  Hurtiglån         (status=Utlånt/Ledig depending on item.onloan)
+            #  8  Ikke til hjemlån
+            #  1  Til internt bruk
+            # -1  Til klargjøring
+            #  2  Til reparasjon
+            #  4  Til retting
+            if ($notloan == -2 || $notloan == -1 || $notloan == 2 || $notloan == 4 ) {
+                $item->{status} = "Utlånt";
+            } elsif ($notloan == 1) {
+                $item->{status} = "Utilgjengelig";
+                $item->{reservable} = 0;
+            } elsif ($notloan == 8) {
+                $item->{status} = "Ikke til hjemlån";
+                $item->{reservable} = 0;
+            } elsif ($notloan == 5) {
+                $item->{reservable} = 0;
+            }
+        }
+
+        if ($item->{itemlost}) {
+            $item->{status} = "Utilgjengelig";
+            $item->{reservable} = 0;
+        }
+
+        my $holds = Koha::Holds->search({itemnumber => $item->{itemnumber}})->unblessed;
+
+        # mark as checked out if item marked as hold
+        if (scalar(@{$holds}) > 0) {
+            $item->{status} = "Utlånt";
+        }
+    }
+
+    return $c->$cb({ biblio => $biblio->unblessed, items => $expanded }, 200);
+}
+
+sub add {
+    my ($c, $args, $cb) = @_;
+
+    my $biblionumber;
+    my $biblioitemnumber;
+
+    my $body = $c->req->body;
+    unless ($body) {
+        return $c->$cb({error => "Missing MARCXML body"}, 400);
+    }
+
+    my $record = eval {MARC::Record::new_from_xml( $body, "utf8", '')};
+    if ($@) {
+        return $c->$cb({error => $@}, 400);
+    } else {
+        my $framework = $c->req->headers->header('framework');
+        ( $biblionumber, $biblioitemnumber ) = &AddBiblio($record, $framework || '');
+    }
+    if ($biblionumber) {
+        $c->res->headers->location($c->url_for('/api/v1/biblios/')->to_abs . $biblionumber);
+        my ( $itemnumbers, $errors ) = &AddItemBatchFromMarc( $record, $biblionumber, $biblioitemnumber, '' );
+        unless (@{$errors}) {
+            return $c->$cb({biblionumber => $biblionumber, items => join(",", @{$itemnumbers})}, 201);
+        } else {
+            warn Dumper($errors);
+            return $c->$cb({error => "Error creating items, see Koha Logs for details.", biblionumber => $biblionumber, items => join(",", @{$itemnumbers})}, 400);
+        }
+    } else {
+        return $c->$cb({error => "unable to create record"}, 400);
+    }
+}
+
+# This will not update items, Items is a separate API route
+sub update {
+    my ($c, $args, $cb) = @_;
+
+    my $biblionumber = $args->{biblionumber};
+
+    my $biblio = Koha::Biblios->find($biblionumber);
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+
+    my $success;
+    my $body = $c->req->body;
+    my $record = eval {MARC::Record::new_from_xml( $body, "utf8", '')};
+    if ($@) {
+        return $c->$cb({error => $@}, 400);
+    } else {
+        $success = &ModBiblio($record, $biblionumber, '');
+    }
+    if ($success) {
+        $c->res->headers->location($c->url_for('/api/v1/biblios/')->to_abs . $biblionumber);
+        return $c->$cb({biblio => Koha::Biblios->find($biblionumber)->unblessed}, 200);
+    } else {
+        return $c->$cb({error => "unable to update record"}, 400);
+    }
+}
+
+sub delete {
+    my ($c, $args, $cb) = @_;
+
+    my $biblio = Koha::Biblios->find($args->{biblionumber});
+    unless ($biblio) {
+        return $c->$cb({error => "Biblio not found"}, 404);
+    }
+    my @items = $biblio->items;
+    # Delete items first
+    my @item_errors = ();
+    foreach my $item (@items) {
+        my $res = $item->delete;
+        unless ($res eq 1) {
+            push @item_errors, $item->unblessed->{itemnumber};
+        }
+    }
+    my $res = $biblio->delete;
+    if ($res eq '1') {
+        return $c->$cb({}, 200);
+    } elsif ($res eq '-1') {
+        return $c->$cb({error => "Not found. Error code: " . $res, items => @item_errors}, 404);
+    } else {
+        return $c->$cb({error => "Error code: " . $res, items => @item_errors}, 400);
+    }
+}
+
+1;
diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json
index 0a00e97..d1cd3eb 100644
--- a/api/v1/swagger/definitions.json
+++ b/api/v1/swagger/definitions.json
@@ -1,4 +1,10 @@
 {
+  "biblios": {
+    "$ref": "definitions/biblios.json"
+  },
+  "biblio": {
+    "$ref": "definitions/biblio.json"
+  },
   "items": {
     "$ref": "definitions/items.json"
   },
diff --git a/api/v1/swagger/definitions/biblios.json b/api/v1/swagger/definitions/biblios.json
new file mode 100644
index 0000000..b0600e8
--- /dev/null
+++ b/api/v1/swagger/definitions/biblios.json
@@ -0,0 +1,4 @@
+{
+    "type": "array",
+    "items": { "$ref": "biblio.json" }
+}
diff --git a/api/v1/swagger/definitions/biblio.json b/api/v1/swagger/definitions/biblio.json
new file mode 100644
index 0000000..bde3f8f
--- /dev/null
+++ b/api/v1/swagger/definitions/biblio.json
@@ -0,0 +1,48 @@
+{
+  "type": "object",
+    "properties": {
+    "biblionumber": {
+      "$ref": "../x-primitives.json#/biblionumber"
+    },
+    "author": {
+      "type": ["string", "null"],
+      "description": "statement of responsibility from MARC record (100$a in MARC21)"
+    },
+    "title": {
+      "type": ["string", "null"],
+      "description": "title (without the subtitle) from the MARC record (245$a in MARC21)"
+    },
+    "unititle": {
+      "type": ["string", "null"],
+      "description": "uniform title (without the subtitle) from the MARC record (240$a in MARC21)"
+    },
+    "notes": {
+      "type": ["string", "null"],
+      "description": "values from the general notes field in the MARC record (500$a in MARC21) split by bar (|)"
+    },
+    "serial": {
+      "type": ["string", "null"],
+      "description": "Boolean indicating whether biblio is for a serial"
+    },
+    "seriestitle": {
+      "type": ["string", "null"],
+      "description": ""
+    },
+    "copyrightdate": {
+      "type": ["string", "null"],
+      "description": "publication or copyright date from the MARC record"
+    },
+    "timestamp": {
+      "type": ["string", "null"],
+      "description": "date and time this record was last touched"
+    },
+    "datecreated": {
+      "type": ["string", "null"],
+      "description": "the date this record was added to Koha"
+    },
+    "abstract": {
+      "type": ["string", "null"],
+      "description": "summary from the MARC record (520$a in MARC21)"
+    }
+  }
+}
diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json
index ce79d4c..4ed7de0 100644
--- a/api/v1/swagger/parameters.json
+++ b/api/v1/swagger/parameters.json
@@ -1,4 +1,7 @@
 {
+  "biblionumberPathParam": {
+    "$ref": "parameters/biblio.json#/biblionumberPathParam"
+   },
   "itemnumberPathParam": {
     "$ref": "parameters/item.json#/itemnumberPathParam"
   },
diff --git a/api/v1/swagger/parameters/biblio.json b/api/v1/swagger/parameters/biblio.json
new file mode 100644
index 0000000..1d1316a
--- /dev/null
+++ b/api/v1/swagger/parameters/biblio.json
@@ -0,0 +1,9 @@
+{
+   "biblionumberPathParam": {
+     "name": "biblionumber",
+     "in": "path",
+     "description": "Internal biblio identifier",
+     "required": true,
+     "type": "integer"
+  }
+}
diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json
index 532eb0d..d768999 100644
--- a/api/v1/swagger/paths.json
+++ b/api/v1/swagger/paths.json
@@ -1,4 +1,16 @@
 {
+  "/biblios": {
+    "$ref": "paths/biblios.json#/~1biblios"
+  },
+  "/biblios/{biblionumber}": {
+    "$ref": "paths/biblios.json#/~1biblios~1{biblionumber}"
+  },
+  "/biblios/{biblionumber}/items": {
+    "$ref": "paths/biblios.json#/~1biblios~1{biblionumber}~1items"
+  },
+  "/biblios/{biblionumber}/expanded": {
+    "$ref": "paths/biblios.json#/~1biblios~1{biblionumber}~1expanded"
+  },
   "/items/{itemnumber}": {
     "$ref": "paths/items.json#/~1items~1{itemnumber}"
   },
diff --git a/api/v1/swagger/paths/biblios.json b/api/v1/swagger/paths/biblios.json
new file mode 100644
index 0000000..d58f205
--- /dev/null
+++ b/api/v1/swagger/paths/biblios.json
@@ -0,0 +1,153 @@
+{
+  "/biblios/{biblionumber}": {
+    "get": {
+      "operationId": "getBiblio",
+      "tags": ["biblios"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": [
+        "application/json"
+      ],
+      "responses": {
+        "200": {
+          "description": "A biblio record",
+          "schema": { "$ref": "../definitions.json#/biblio" }
+
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": {
+            "$ref": "../definitions.json#/error"
+          }
+        }
+      }
+    },
+    "put": {
+      "operationId": "updateBiblio",
+      "tags": ["biblios"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": [
+        "application/json"
+      ],
+      "responses": {
+        "200": {
+          "description": "An updated biblio record"
+        },
+        "400": {
+          "description": "Biblio update failed",
+          "schema": { "$ref": "../definitions.json#/error" }
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": {
+            "$ref": "../definitions.json#/error"
+          }
+        }
+      },
+      "x-koha-authorization": {
+        "permissions": {
+          "editcatalogue": "1"
+        }
+      }
+    },
+    "delete": {
+      "operationId": "deleteBiblio",
+      "tags": ["biblios"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": ["application/json"],
+      "responses": {
+        "200": {
+          "description": "Biblio record deleted successfully",
+          "schema": {
+            "type": "object"
+          }
+        },
+        "400": {
+          "description": "Biblio deletion failed",
+          "schema": { "$ref": "../definitions.json#/error" }
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": { "$ref": "../definitions.json#/error" }
+        }
+      },
+      "x-koha-authorization": {
+        "permissions": {
+          "editcatalogue": "1"
+        }
+      }
+    }
+  },
+  "/biblios/{biblionumber}/items": {
+    "get": {
+      "operationId": "getitemsByBiblio",
+      "tags": ["biblios", "items"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": [
+        "application/json"
+      ],
+      "responses": {
+        "200": {
+          "description": "A biblio record with items"
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": {
+            "$ref": "../definitions.json#/error"
+          }
+        }
+      }
+    }
+  },
+  "/biblios/{biblionumber}/expanded": {
+    "get": {
+      "operationId": "getexpandedByBiblio",
+      "tags": ["biblios", "items", "item status"],
+      "parameters": [
+        { "$ref": "../parameters.json#/biblionumberPathParam" }
+      ],
+      "produces": [
+        "application/json"
+      ],
+      "responses": {
+        "200": {
+          "description": "A biblio record with items and item statuses"
+        },
+        "404": {
+          "description": "Biblio not found",
+          "schema": {
+            "$ref": "../definitions.json#/error"
+          }
+        }
+      }
+    }
+  },
+  "/biblios": {
+    "post": {
+      "operationId": "addBiblio",
+      "tags": ["biblios"],
+      "produces": ["application/json"],
+      "responses": {
+        "201": {
+          "description": "A new biblio record"
+        },
+        "400": {
+          "description": "Unable to create biblio record",
+          "schema": { "$ref": "../definitions.json#/error" }
+        }
+      }
+    },
+    "x-koha-authorization": {
+      "permissions": {
+        "editcatalogue": "1"
+      }
+    }
+  }
+}

{'fraction_non_alphanumeric': 0.21093588595826734, 'fraction_numerical': 0.025445905929343707, 'mean_word_length': 2.459559261465158, 'pattern_counts': {'":': 328, '<': 3, '<?xml version=': 0, '>': 208, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 10, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6610923', 'n_tokens_mistral': 11455, 'n_tokens_neox': 10573, 'n_words': 2112}