001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.iso;
020
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.List;
024import java.util.Objects;
025
026/**
027 * Encodes and decodes the ISO 8583 Message Error Indicator (DE-018).
028 *
029 * <p>DE-018 carries up to ten error sets, each exactly 14 positions long.
030 * Each set identifies the type of error and the precise location within the
031 * message (data element, sub-element for constructed fields, or dataset
032 * identifier plus bit/tag for composite fields).</p>
033 *
034 * <p>Error sets are concatenated without separators. The overall field is
035 * transmitted as an {@code LLLVAR} character field.</p>
036 *
037 * <h2>Error set wire layout</h2>
038 * <pre>
039 *  Pos  Len  Type        Subfield
040 *  1–2    2  N2 (ASCII)  Error severity
041 *  3–6    4  N4 (ASCII)  Message error code
042 *  7–9    3  N3 (ASCII)  Data element in error (001–128)
043 * 10–11   2  N2 (ASCII)  Data sub-element in error (constructed DEs), else "00"
044 *   12    1  B1 (binary) Dataset identifier (composite DEs), else 0x00
045 * 13–14   2  B2 (binary) Dataset bit or TLV tag (composite DEs), else 0x0000
046 * </pre>
047 *
048 * <p>Usage:</p>
049 * <pre>
050 * MessageErrorIndicator mei = new MessageErrorIndicator()
051 *     .add(FieldError.primitiveError(ErrorCode.REQUIRED_MISSING, 37))
052 *     .add(FieldError.compositeError(ErrorCode.INVALID_VALUE, 55, 0x37, 0x9F26));
053 *
054 * msg.set(18, new String(mei.pack(), ISOUtil.CHARSET));
055 *
056 * // or
057 *
058 * MessageErrorIndicator parsed = MessageErrorIndicator.unpack(msg.getBytes(18));
059 * </pre>
060 *
061 * @see <a href="https://jpos.org/doc/jPOS-CMF.pdf">jPOS CMF Specification — DE-018</a>
062 */
063public class MessageErrorIndicator {
064
065    /** Maximum number of error sets per DE-018 field. */
066    public static final int MAX_ERROR_SETS = 10;
067
068    /** Length in bytes of one error set on the wire. */
069    public static final int ERROR_SET_LENGTH = 14;
070
071    private final List<FieldError> errors = new ArrayList<>();
072
073    /**
074     * Creates an empty indicator.
075     */
076    public MessageErrorIndicator() {
077    }
078
079    /**
080     * Appends an error set.
081     *
082     * @param error error set to add
083     * @return this indicator for fluent chaining
084     * @throws IllegalStateException when the maximum of 10 error sets has been reached
085     */
086    public MessageErrorIndicator add(FieldError error) {
087        if (errors.size() >= MAX_ERROR_SETS) {
088            throw new IllegalStateException("DE-018 may carry at most " + MAX_ERROR_SETS + " error sets");
089        }
090        Objects.requireNonNull(error, "error cannot be null");
091        errors.add(error);
092        return this;
093    }
094
095    /**
096     * Returns an unmodifiable view of the current error sets.
097     *
098     * @return error set list
099     */
100    public List<FieldError> errors() {
101        return Collections.unmodifiableList(errors);
102    }
103
104    /**
105     * Indicates whether this indicator contains no error sets.
106     *
107     * @return {@code true} when empty
108     */
109    public boolean isEmpty() {
110        return errors.isEmpty();
111    }
112
113    /**
114     * Returns the number of error sets.
115     *
116     * @return error set count
117     */
118    public int size() {
119        return errors.size();
120    }
121
122    /**
123     * Serializes all error sets to the DE-018 wire format.
124     *
125     * @return packed bytes suitable for setting on DE-018 of an ISOMsg
126     */
127    public byte[] pack() {
128        byte[] result = new byte[errors.size() * ERROR_SET_LENGTH];
129        int offset = 0;
130        for (FieldError error : errors) {
131            byte[] set = error.pack();
132            System.arraycopy(set, 0, result, offset, ERROR_SET_LENGTH);
133            offset += ERROR_SET_LENGTH;
134        }
135        return result;
136    }
137
138    /**
139     * Deserializes DE-018 wire bytes into a {@code MessageErrorIndicator}.
140     *
141     * @param data DE-018 field bytes (must be a multiple of 14)
142     * @return parsed indicator
143     * @throws ISOException when the byte array is malformed
144     */
145    public static MessageErrorIndicator unpack(byte[] data) throws ISOException {
146        if (data == null || data.length == 0) {
147            return new MessageErrorIndicator();
148        }
149        if (data.length % ERROR_SET_LENGTH != 0) {
150            throw new ISOException("DE-018 length " + data.length
151                    + " is not a multiple of " + ERROR_SET_LENGTH);
152        }
153        int count = data.length / ERROR_SET_LENGTH;
154        if (count > MAX_ERROR_SETS) {
155            throw new ISOException("DE-018 contains " + count
156                    + " error sets, maximum is " + MAX_ERROR_SETS);
157        }
158        MessageErrorIndicator mei = new MessageErrorIndicator();
159        for (int i = 0; i < count; i++) {
160            int off = i * ERROR_SET_LENGTH;
161            mei.errors.add(FieldError.unpack(data, off));
162        }
163        return mei;
164    }
165
166    // ─────────────────────────────────────────────────────────────────────────
167
168    /**
169     * Standard message error codes defined by ISO 8583:2023, Table D.15.
170     *
171     * <p>Codes 0014–3999 are reserved for ISO use; 4000–5999 for national use;
172     * 6000–9999 for private use.</p>
173     */
174    public enum ErrorCode {
175        /** Required data element is missing. */
176        REQUIRED_MISSING(1),
177        /** Data element length is invalid. */
178        INVALID_LENGTH(2),
179        /** Data element contains an invalid value. */
180        INVALID_VALUE(3),
181        /** Amount field has a format error. */
182        AMOUNT_FORMAT(4),
183        /** Date field has a format error. */
184        DATE_FORMAT(5),
185        /** Account identifier has a format error. */
186        ACCOUNT_FORMAT(6),
187        /** Name field has a format error. */
188        NAME_FORMAT(7),
189        /** Other format error. */
190        FORMAT_OTHER(8),
191        /** Data inconsistent with POS data code. */
192        INCONSISTENT_WITH_POS_CODE(9),
193        /** Data does not match the original request. */
194        INCONSISTENT_WITH_ORIGINAL(10),
195        /** Other inconsistent data. */
196        INCONSISTENT_OTHER(11),
197        /** Recurring data error. */
198        RECURRING_DATA(12),
199        /** Customer vendor format error. */
200        CUSTOMER_VENDOR_FORMAT(13);
201
202        private final int code;
203
204        ErrorCode(int code) {
205            this.code = code;
206        }
207
208        /**
209         * Returns the 4-digit numeric code as carried on the wire.
210         *
211         * @return numeric code
212         */
213        public int code() {
214            return code;
215        }
216
217        /**
218         * Returns the error code as a 4-character left-zero-padded ASCII string.
219         *
220         * @return wire-format code string
221         */
222        public String codeString() {
223            return String.format("%04d", code);
224        }
225
226        /**
227         * Resolves an {@link ErrorCode} from its numeric value.
228         *
229         * @param code numeric code
230         * @return matching constant or {@code null} when not in the ISO-defined range
231         */
232        public static ErrorCode of(int code) {
233            for (ErrorCode ec : values()) {
234                if (ec.code == code) {
235                    return ec;
236                }
237            }
238            return null;
239        }
240    }
241
242    /**
243     * Error severity carried in positions 1–2 of each error set.
244     */
245    public enum Severity {
246        /** The message was rejected due to this error. */
247        REJECTED(0),
248        /** The message was accepted but contains a non-critical error. */
249        WARNING(1);
250
251        private final int value;
252
253        Severity(int value) {
254            this.value = value;
255        }
256
257        /**
258         * Returns the 2-digit numeric value as carried on the wire.
259         *
260         * @return numeric severity value
261         */
262        public int value() {
263            return value;
264        }
265
266        /**
267         * Resolves a {@link Severity} from its numeric value.
268         *
269         * @param value 0 or 1
270         * @return matching constant, defaulting to {@link #REJECTED} for unknown values
271         */
272        public static Severity of(int value) {
273            for (Severity s : values()) {
274                if (s.value == value) {
275                    return s;
276                }
277            }
278            return REJECTED;
279        }
280    }
281
282    // ─────────────────────────────────────────────────────────────────────────
283
284    /**
285     * One error set within a {@link MessageErrorIndicator} field.
286     *
287     * <p>The wire layout is exactly 14 bytes:</p>
288     * <ul>
289     *   <li>2 ASCII digits — severity</li>
290     *   <li>4 ASCII digits — message error code</li>
291     *   <li>3 ASCII digits — data element number (001–128)</li>
292     *   <li>2 ASCII digits — sub-element (constructed DEs) or "00"</li>
293     *   <li>1 binary byte — dataset identifier (composite DEs) or 0x00</li>
294     *   <li>2 binary bytes — dataset bit or TLV tag (composite DEs) or 0x0000</li>
295     * </ul>
296     */
297    public static class FieldError {
298
299        private final Severity severity;
300        private final int errorCode;
301        private final int deNumber;
302        private final int subElement;
303        private final int datasetIdentifier;
304        private final int datasetBitOrTag;
305
306        private FieldError(Severity severity, int errorCode, int deNumber,
307                           int subElement, int datasetIdentifier, int datasetBitOrTag) {
308            if (deNumber < 1 || deNumber > 128) {
309                throw new IllegalArgumentException("deNumber must be 1–128");
310            }
311            this.severity = severity;
312            this.errorCode = errorCode;
313            this.deNumber = deNumber;
314            this.subElement = subElement;
315            this.datasetIdentifier = datasetIdentifier;
316            this.datasetBitOrTag = datasetBitOrTag;
317        }
318
319        /**
320         * Creates an error set for a primitive data element.
321         *
322         * @param errorCode   error code
323         * @param deNumber    data element number (1–128)
324         * @return new error set
325         */
326        public static FieldError primitiveError(ErrorCode errorCode, int deNumber) {
327            return new FieldError(Severity.REJECTED, errorCode.code(), deNumber, 0, 0, 0);
328        }
329
330        /**
331         * Creates an error set for a primitive data element with a specified severity.
332         *
333         * @param severity    error severity
334         * @param errorCode   error code
335         * @param deNumber    data element number (1–128)
336         * @return new error set
337         */
338        public static FieldError primitiveError(Severity severity, ErrorCode errorCode, int deNumber) {
339            return new FieldError(severity, errorCode.code(), deNumber, 0, 0, 0);
340        }
341
342        /**
343         * Creates an error set for a sub-element within a constructed data element.
344         *
345         * @param errorCode   error code
346         * @param deNumber    data element number (1–128)
347         * @param subElement  sub-element part number (1-based)
348         * @return new error set
349         */
350        public static FieldError constructedError(ErrorCode errorCode, int deNumber, int subElement) {
351            return new FieldError(Severity.REJECTED, errorCode.code(), deNumber, subElement, 0, 0);
352        }
353
354        /**
355         * Creates an error set for a sub-element within a composite (dataset) data element.
356         *
357         * @param errorCode         error code
358         * @param deNumber          data element number (1–128)
359         * @param datasetIdentifier dataset identifier byte (0x01–0xFE)
360         * @param datasetBitOrTag   DBM bit number or BER-TLV tag (packed into 2 bytes big-endian)
361         * @return new error set
362         */
363        public static FieldError compositeError(ErrorCode errorCode, int deNumber,
364                                                int datasetIdentifier, int datasetBitOrTag) {
365            return new FieldError(Severity.REJECTED, errorCode.code(), deNumber, 0,
366                    datasetIdentifier, datasetBitOrTag);
367        }
368
369        /**
370         * Creates an error set with a raw numeric error code (for private-use or
371         * national-use codes outside the ISO-defined {@link ErrorCode} enum).
372         *
373         * @param severity    error severity
374         * @param errorCode   raw 4-digit error code (0001–9999)
375         * @param deNumber    data element number (1–128)
376         * @return new error set
377         */
378        public static FieldError withRawCode(Severity severity, int errorCode, int deNumber) {
379            return new FieldError(severity, errorCode, deNumber, 0, 0, 0);
380        }
381
382        /**
383         * Returns the severity of this error set.
384         *
385         * @return error severity
386         */
387        public Severity severity() { return severity; }
388
389        /**
390         * Returns the raw 4-digit error code value carried on the wire.
391         *
392         * @return message error code numeric value
393         */
394        public int errorCode() { return errorCode; }
395
396        /**
397         * Returns the {@link ErrorCode} matching {@link #errorCode()}, if any.
398         *
399         * @return resolved {@link ErrorCode} or {@code null} for private/national-use codes
400         */
401        public ErrorCode errorCodeEnum() { return ErrorCode.of(errorCode); }
402
403        /**
404         * Returns the data element number flagged by this error.
405         *
406         * @return data element number in error
407         */
408        public int deNumber() { return deNumber; }
409
410        /**
411         * Returns the sub-element number for constructed data elements.
412         *
413         * @return sub-element number (constructed DEs), 0 otherwise
414         */
415        public int subElement() { return subElement; }
416
417        /**
418         * Returns the dataset identifier byte for composite data elements.
419         *
420         * @return dataset identifier (composite DEs), 0 otherwise
421         */
422        public int datasetIdentifier() { return datasetIdentifier; }
423
424        /**
425         * Returns the DBM bit number or BER-TLV tag for composite data elements.
426         *
427         * @return dataset bit number or TLV tag (composite DEs), 0 otherwise
428         */
429        public int datasetBitOrTag() { return datasetBitOrTag; }
430
431        /**
432         * Serializes this error set to exactly 14 bytes.
433         *
434         * @return 14-byte wire representation
435         */
436        public byte[] pack() {
437            byte[] buf = new byte[ERROR_SET_LENGTH];
438            // Severity: 2 ASCII digits
439            buf[0] = (byte) ('0' + (severity.value() / 10));
440            buf[1] = (byte) ('0' + (severity.value() % 10));
441            // Error code: 4 ASCII digits
442            String codeStr = String.format("%04d", errorCode);
443            buf[2] = (byte) codeStr.charAt(0);
444            buf[3] = (byte) codeStr.charAt(1);
445            buf[4] = (byte) codeStr.charAt(2);
446            buf[5] = (byte) codeStr.charAt(3);
447            // DE number: 3 ASCII digits
448            String deStr = String.format("%03d", deNumber);
449            buf[6] = (byte) deStr.charAt(0);
450            buf[7] = (byte) deStr.charAt(1);
451            buf[8] = (byte) deStr.charAt(2);
452            // Sub-element: 2 ASCII digits
453            String seStr = String.format("%02d", subElement);
454            buf[9]  = (byte) seStr.charAt(0);
455            buf[10] = (byte) seStr.charAt(1);
456            // Dataset identifier: 1 binary byte
457            buf[11] = (byte) (datasetIdentifier & 0xFF);
458            // Dataset bit/tag: 2 binary bytes big-endian
459            buf[12] = (byte) ((datasetBitOrTag >> 8) & 0xFF);
460            buf[13] = (byte) (datasetBitOrTag & 0xFF);
461            return buf;
462        }
463
464        /**
465         * Deserializes one error set from 14 bytes at the given offset.
466         *
467         * @param data   source buffer
468         * @param offset starting offset
469         * @return parsed error set
470         * @throws ISOException on malformed data
471         */
472        static FieldError unpack(byte[] data, int offset) throws ISOException {
473            if (data.length - offset < ERROR_SET_LENGTH) {
474                throw new ISOException("Insufficient data for DE-018 error set at offset " + offset);
475            }
476            try {
477                int sev      = Integer.parseInt(new String(data, offset, 2));
478                int code     = Integer.parseInt(new String(data, offset + 2, 4));
479                int deNum    = Integer.parseInt(new String(data, offset + 6, 3));
480                int subEl    = Integer.parseInt(new String(data, offset + 9, 2));
481                int dsId     = data[offset + 11] & 0xFF;
482                int bitOrTag = ((data[offset + 12] & 0xFF) << 8) | (data[offset + 13] & 0xFF);
483                return new FieldError(Severity.of(sev), code, deNum, subEl, dsId, bitOrTag);
484            } catch (NumberFormatException e) {
485                throw new ISOException("Malformed DE-018 error set at offset " + offset, e);
486            }
487        }
488
489        @Override
490        public String toString() {
491            ErrorCode ec = errorCodeEnum();
492            return "FieldError{sev=" + severity
493                    + ", code=" + String.format("%04d", errorCode)
494                    + (ec != null ? "(" + ec.name() + ")" : "")
495                    + ", de=" + String.format("%03d", deNumber)
496                    + (subElement != 0 ? ", sub=" + subElement : "")
497                    + (datasetIdentifier != 0 ? String.format(", dsId=0x%02X", datasetIdentifier) : "")
498                    + (datasetBitOrTag != 0 ? String.format(", bitOrTag=0x%04X", datasetBitOrTag) : "")
499                    + "}";
500        }
501    }
502}