Bug 40383: Add JSON representation to Koha::Edifact and JSON download to edimsg.pl

This patch implements a JSON representation of EDIFACT interchanges for enhanced
programmatic access and debugging capabilities.

Backend Changes:
- Add to_json() method to Koha::Edifact module
- Structured JSON output with header, messages array, and trailer
- Segment parsing with raw data, elements arrays, and line_id tracking
- Line item grouping using LIN segment identifiers

Frontend Changes:
- Add JSON download button to edimsg.pl page
- Support for ?format=json parameter and Accept: application/json header
- Proper HTTP content-type handling for JSON responses
- Error handling for missing messages in JSON format

JSON Structure:
{
  "header": "UNB+UNOC:3+...",
  "messages": [
    {
      "header": "UNH+1+QUOTES:D:93A:UN:EAN008'",
      "segments": [
        {
          "tag": "LIN",
          "raw": "LIN+1+1+9780552150040:EN'",
          "elements": ["1", "1", "9780552150040:EN"],
          "line_id": "1"
        }
      ],
      "trailer": "UNT+25+1'"
    }
  ],
  "trailer": "UNZ+1+1'"
}

Test plan:
1. Apply this patch
2. Ensure you have EDIFACT messages in your system
3. Go to Acquisitions > EDIFACT messages
4. Click "View message" for any message
5. Click the new "JSON" button
6. Verify JSON downloads with proper structure
7. Verify direct URL access with ?format=json parameter works
8. Test with Accept: application/json header

Signed-off-by: Hannah Dunne-Howrie <hdunne-howrie@westminster.gov.uk>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
This commit is contained in:
Martin Renvoize 2025-07-22 14:08:59 +01:00
parent e8cdc4e9d5
commit 1f5f91e649
No known key found for this signature in database
GPG Key ID: 2D86E0EAEC83711B
3 changed files with 155 additions and 4 deletions

View File

@ -21,6 +21,7 @@ use strict;
use warnings;
use File::Slurp qw( read_file );
use Carp qw( carp croak );
use JSON qw( encode_json );
use Koha::Edifact::Segment;
use Koha::Edifact::Message;
@ -172,6 +173,113 @@ sub message_array {
return $msg_arr;
}
sub to_json {
my $self = shift;
# Find interchange header (UNB)
my $header_seg;
my $trailer_seg;
my @all_segments = @{ $self->{transmission} };
# Get header and trailer
for my $i ( 0 .. $#all_segments ) {
if ( $all_segments[$i]->tag eq 'UNB' ) {
$header_seg = $all_segments[$i];
} elsif ( $all_segments[$i]->tag eq 'UNZ' ) {
$trailer_seg = $all_segments[$i];
}
}
# Build messages array
my @json_messages;
my $current_message;
my @current_segments;
my $current_line_id;
for my $segment (@all_segments) {
my $tag = $segment->tag;
if ( $tag eq 'UNH' ) {
# Start new message
if ($current_message) {
$current_message->{segments} = [@current_segments];
push @json_messages, $current_message;
}
$current_message = {
header => $segment->as_string,
segments => [],
};
@current_segments = ();
$current_line_id = undef;
} elsif ( $tag eq 'UNT' ) {
# End current message
if ($current_message) {
$current_message->{trailer} = $segment->as_string;
$current_message->{segments} = [@current_segments];
push @json_messages, $current_message;
}
$current_message = undef;
@current_segments = ();
$current_line_id = undef;
} elsif ( $tag eq 'UNB' || $tag eq 'UNZ' ) {
# Skip - these are handled separately
next;
} elsif ($current_message) {
# Process segment within message
my $segment_data = {
tag => $tag,
raw => $segment->as_string,
elements => [],
};
# Extract elements
my $elem_count = 0;
while ( defined( my $elem = $segment->elem($elem_count) ) ) {
push @{ $segment_data->{elements} }, $elem;
$elem_count++;
}
# Handle line_id assignment
if ( $tag eq 'LIN' ) {
$current_line_id = $segment->elem(0);
$segment_data->{line_id} = $current_line_id if defined $current_line_id;
} elsif ( $tag eq 'UNS' ) {
# UNS (Section Control) marks transition to message summary
# Reset line_id so subsequent segments are treated as message-level
$current_line_id = undef;
} elsif ( defined $current_line_id && $tag !~ /^(UNH|UNT|UNS|CNT)$/ ) {
# Assign line_id to segments that belong to current line item
# Exclude message control segments (UNH, UNT, UNS, CNT)
# After UNS, current_line_id is undefined, so segments are message-level
$segment_data->{line_id} = $current_line_id;
}
push @current_segments, $segment_data;
}
}
# Handle any remaining message
if ($current_message) {
$current_message->{segments} = [@current_segments];
push @json_messages, $current_message;
}
# Build final structure
my $json_structure = {
header => $header_seg ? $header_seg->as_string : undef,
messages => \@json_messages,
trailer => $trailer_seg ? $trailer_seg->as_string : undef,
};
return encode_json($json_structure);
}
#
# internal parsing routines used in _init
#
@ -291,6 +399,13 @@ Edifact - Edifact message handler
return an array of Message objects contained in the Edifact transmission
=head2 to_json
returns a JSON representation of the complete edifact interchange.
The JSON structure includes the interchange header, all messages with their
segments (both raw and parsed), and the interchange trailer.
Line items are grouped using line_id extracted from LIN segments.
=head1 Internal Methods
=head2 _init

View File

@ -20,11 +20,18 @@ use Modern::Perl;
use CGI;
use Koha::Database;
use Koha::Edifact;
use C4::Koha;
use C4::Auth qw( get_template_and_user );
use C4::Output qw( output_html_with_http_headers );
use C4::Output qw( output_html_with_http_headers output_with_http_headers );
my $q = CGI->new;
# Check if JSON output is requested
my $format = $q->param('format') || '';
my $accept_header = $ENV{HTTP_ACCEPT} || '';
my $wants_json = ( $format eq 'json' ) || ( $accept_header =~ m|application/json| );
my ( $template, $loggedinuser, $cookie, $userflags ) = get_template_and_user(
{
template_name => 'acqui/edimsg.tt',
@ -33,17 +40,39 @@ my ( $template, $loggedinuser, $cookie, $userflags ) = get_template_and_user(
flagsrequired => { acquisition => 'edi_manage' },
}
);
my $msg_id = $q->param('id');
my $schema = Koha::Database->new()->schema();
my $msg = $schema->resultset('EdifactMessage')->find($msg_id);
if ($msg) {
my $transmission = $msg->raw_msg;
my @segments = segmentize($transmission);
$template->param( segments => \@segments );
if ($wants_json) {
# Return JSON representation using Koha::Edifact
my $edifact = Koha::Edifact->new( { transmission => $transmission } );
my $json_output = $edifact->to_json();
output_with_http_headers( $q, $cookie, $json_output, 'json', '200 OK' );
exit;
} else {
# Return HTML representation (existing behavior)
my @segments = segmentize($transmission);
$template->param(
segments => \@segments,
id => $msg_id
);
}
} else {
$template->param( no_message => 1 );
if ($wants_json) {
output_with_http_headers( $q, $cookie, '{"error":"Message not found"}', 'json', '404 Not Found' );
exit;
} else {
$template->param( no_message => 1 );
}
}
output_html_with_http_headers( $q, $cookie, $template->output );

View File

@ -32,6 +32,13 @@
[% IF no_message %]
<div class="alert alert-info">The requested message cannot be displayed</div>
[% ELSE %]
<div class="row">
<div class="col-sm-2">
<div id="toolbar" class="btn-toolbar">
<a class="btn btn-default btn-sm" href="[% "/cgi-bin/koha/acqui/edimsg.pl?id=" _ id _ "&format=json" %]" target="_blank"> <i class="fa fa-download"></i> JSON </a>
</div>
</div>
</div>
<ul>
[% FOREACH seg IN segments %]
<li>[% seg | html %]</li>