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 org.jpos.transaction.TxnId;
022
023import java.util.Optional;
024
025/**
026 * Encodes and decodes the ISO 8583 Transaction Life Cycle Identification Data
027 * (DE-021).
028 *
029 * <p>DE-021 is a 19-byte constructed field that correlates related messages
030 * across a transaction's full life cycle—authorization, financial presentment,
031 * reversal, and chargeback.</p>
032 *
033 * <h2>Wire layout</h2>
034 * <pre>
035 *  Bytes  Notation     Sub-field
036 *  1       AN1 (ASCII)  Life cycle support indicator
037 *  2–16   AN15 (ASCII)  Life cycle trace identifier
038 *  17–18  N2 (BCD)     Life cycle transaction sequence number
039 *  19–22  N4 (BCD)     Life cycle authentication token
040 * </pre>
041 *
042 * <h2>jPOS TxnId integration</h2>
043 * <p>When jPOS originates a transaction, the trace identifier is populated from
044 * a {@link TxnId} via its base-36 {@link TxnId#toRrn()} form, right-padded with
045 * spaces to 15 characters. On inbound messages, {@link #txnId()} attempts to
046 * reverse this mapping; it returns an empty {@link Optional} when the trace
047 * identifier does not represent a {@code TxnId} (e.g. when received from an
048 * external network with its own trace ID scheme).</p>
049 *
050 * <h2>Usage — outbound (originates authorization)</h2>
051 * <pre>
052 * byte[] de21 = LifeCycleId.builder()
053 *     .supportIndicator(mti)        // derives '1' for 1xx, '2' for 2xx, etc.
054 *     .traceId(txnId)               // uses txnId.toRrn(), padded to 15 chars
055 *     .build()
056 *     .pack();
057 * msg.set(new ISOBinaryField(21, de21));
058 * </pre>
059 *
060 * <h2>Usage — outbound (financial presentment echoing prior auth)</h2>
061 * <pre>
062 * LifeCycleId authLC = LifeCycleId.unpack(authMsg.getBytes(21));
063 * byte[] de21 = authLC.toBuilder()
064 *     .sequenceNumber(seq)
065 *     .authToken(tokenFromAuthResponse)
066 *     .build()
067 *     .pack();
068 * </pre>
069 *
070 * <h2>Usage — inbound (from external network)</h2>
071 * <pre>
072 * LifeCycleId lc = LifeCycleId.unpack(msg.getBytes(21));
073 * lc.txnId().ifPresent(t -> ctx.put("TXNID", t));
074 * String rawTrace = lc.traceIdentifier();
075 * </pre>
076 *
077 * @see <a href="https://jpos.org/doc/jPOS-CMF.pdf">jPOS CMF Specification — DE-021</a>
078 */
079public final class LifeCycleId {
080
081    /** Wire length of the full DE-021 field in bytes. */
082    public static final int WIRE_LENGTH = 19;
083
084    /** Maximum trace identifier length in characters. */
085    public static final int TRACE_ID_LENGTH = 15;
086
087    private static final int SEQ_OFFSET  = 16;   // 1 + 15 = 16
088    private static final int AUTH_OFFSET = 17;   // 16 + 1 (N2 = 2 digits BCD = 1 byte)
089
090    private final char   supportIndicator;
091    private final String traceIdentifier;
092    private final int    sequenceNumber;
093    private final int    authToken;
094
095    private LifeCycleId(Builder b) {
096        this.supportIndicator = b.supportIndicator;
097        this.traceIdentifier  = b.traceIdentifier;
098        this.sequenceNumber   = b.sequenceNumber;
099        this.authToken        = b.authToken;
100    }
101
102    // ── Accessors ─────────────────────────────────────────────────────────────
103
104    /**
105     * Returns the life cycle support indicator character.
106     * {@code '1'} indicates the identifier was first assigned during an
107     * authorization message; {@code '2'} during a financial presentment.
108     *
109     * @return support indicator
110     */
111    public char supportIndicator() {
112        return supportIndicator;
113    }
114
115    /**
116     * Returns the raw 15-character life cycle trace identifier.
117     *
118     * @return trace identifier
119     */
120    public String traceIdentifier() {
121        return traceIdentifier;
122    }
123
124    /**
125     * Attempts to parse the trace identifier as a jPOS {@link TxnId}.
126     *
127     * <p>Returns a non-empty {@link Optional} when the trimmed trace identifier
128     * was produced by {@link TxnId#toRrn()}. Returns empty for identifiers
129     * generated by other systems.</p>
130     *
131     * @return optional TxnId
132     */
133    public Optional<TxnId> txnId() {
134        try {
135            return Optional.of(TxnId.fromRrn(traceIdentifier.trim()));
136        } catch (Exception e) {
137            return Optional.empty();
138        }
139    }
140
141    /**
142     * Returns the life cycle transaction sequence number.
143     * Zero when not assigned.
144     *
145     * @return sequence number
146     */
147    public int sequenceNumber() {
148        return sequenceNumber;
149    }
150
151    /**
152     * Returns the life cycle authentication token assigned by the card issuer.
153     * Zero when not yet assigned or when omitted by mutual agreement.
154     *
155     * @return authentication token
156     */
157    public int authToken() {
158        return authToken;
159    }
160
161    // ── Pack / Unpack ─────────────────────────────────────────────────────────
162
163    /**
164     * Serializes this life cycle identifier to the DE-021 wire format.
165     *
166     * @return 19-byte wire representation
167     */
168    public byte[] pack() {
169        byte[] buf = new byte[WIRE_LENGTH];
170        buf[0] = (byte) supportIndicator;
171        byte[] trace = traceIdentifier.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
172        System.arraycopy(trace, 0, buf, 1, TRACE_ID_LENGTH);
173        // Sequence number: N2 in cmf.xml = 2 BCD digits = 1 byte
174        ISOUtil.str2bcd(String.format("%02d", sequenceNumber), false, buf, SEQ_OFFSET);
175        // Auth token: N4 in cmf.xml = 4 BCD digits = 2 bytes
176        ISOUtil.str2bcd(String.format("%04d", authToken), false, buf, AUTH_OFFSET);
177        return buf;
178    }
179
180    /**
181     * Deserializes DE-021 bytes into a {@code LifeCycleId}.
182     *
183     * @param data 19-byte DE-021 field content
184     * @return parsed life cycle identifier
185     * @throws ISOException when the byte array is null, empty, or too short
186     */
187    public static LifeCycleId unpack(byte[] data) throws ISOException {
188        if (data == null || data.length < WIRE_LENGTH) {
189            throw new ISOException("DE-021 requires exactly " + WIRE_LENGTH + " bytes, got "
190                    + (data == null ? "null" : data.length));
191        }
192        char indicator = (char) (data[0] & 0xFF);
193        String trace   = new String(data, 1, TRACE_ID_LENGTH,
194                java.nio.charset.StandardCharsets.ISO_8859_1);
195        int seq   = Integer.parseInt(ISOUtil.bcd2str(data, SEQ_OFFSET,  2, false));
196        int token = Integer.parseInt(ISOUtil.bcd2str(data, AUTH_OFFSET, 4, false));
197        return new Builder()
198                .supportIndicator(indicator)
199                .traceId(trace)
200                .sequenceNumber(seq)
201                .authToken(token)
202                .build();
203    }
204
205    // ── Builder ───────────────────────────────────────────────────────────────
206
207    /**
208     * Creates a new {@link Builder} pre-populated with this instance's values.
209     * Useful for constructing a financial presentment that echoes an authorization's
210     * life cycle identifier.
211     *
212     * @return builder initialized from this instance
213     */
214    public Builder toBuilder() {
215        return new Builder()
216                .supportIndicator(supportIndicator)
217                .traceId(traceIdentifier)
218                .sequenceNumber(sequenceNumber)
219                .authToken(authToken);
220    }
221
222    /**
223     * Returns a new empty builder.
224     *
225     * @return builder
226     */
227    public static Builder builder() {
228        return new Builder();
229    }
230
231    /**
232     * Derives the life cycle support indicator character from an ISO 8583 MTI.
233     * Returns the first digit of the MTI's class component (position 1, 0-indexed).
234     *
235     * <p>Examples: {@code "0100"} → {@code '1'}, {@code "0200"} → {@code '2'},
236     * {@code "0420"} → {@code '4'}.</p>
237     *
238     * @param mti 3- or 4-character ISO 8583 MTI string
239     * @return support indicator character
240     * @throws IllegalArgumentException when the MTI is null or too short
241     */
242    public static char supportIndicatorForMTI(String mti) {
243        if (mti == null || mti.length() < 2) {
244            throw new IllegalArgumentException("MTI too short: " + mti);
245        }
246        // For a 4-char MTI (e.g. "0100"), class is digit at index 1.
247        // For a 3-char MTI used in CMF shorthand (e.g. "100"), class is digit at index 0.
248        return mti.length() >= 4 ? mti.charAt(1) : mti.charAt(0);
249    }
250
251    // ─────────────────────────────────────────────────────────────────────────
252
253    /**
254     * Builder for {@link LifeCycleId}.
255     */
256    public static final class Builder {
257        private char   supportIndicator = '0';
258        private String traceIdentifier  = padTrace("");
259        private int    sequenceNumber   = 0;
260        private int    authToken        = 0;
261
262        private Builder() {}
263
264        /**
265         * Sets the support indicator from a literal character.
266         *
267         * @param indicator support indicator character
268         * @return this builder
269         */
270        public Builder supportIndicator(char indicator) {
271            this.supportIndicator = indicator;
272            return this;
273        }
274
275        /**
276         * Derives the support indicator from an ISO 8583 MTI string.
277         *
278         * @param mti ISO 8583 MTI (3 or 4 characters)
279         * @return this builder
280         */
281        public Builder supportIndicator(String mti) {
282            this.supportIndicator = supportIndicatorForMTI(mti);
283            return this;
284        }
285
286        /**
287         * Sets the trace identifier from a jPOS {@link TxnId}.
288         * Uses {@link TxnId#toRrn()}, right-padded with spaces to 15 characters.
289         *
290         * @param txnId jPOS transaction identifier
291         * @return this builder
292         */
293        public Builder traceId(TxnId txnId) {
294            this.traceIdentifier = padTrace(txnId.toRrn());
295            return this;
296        }
297
298        /**
299         * Sets the trace identifier from a raw string.
300         * MUST be at most 15 characters; shorter values are right-padded with spaces.
301         *
302         * @param trace trace identifier string
303         * @return this builder
304         * @throws IllegalArgumentException when the string exceeds 15 characters
305         */
306        public Builder traceId(String trace) {
307            if (trace != null && trace.length() > TRACE_ID_LENGTH) {
308                throw new IllegalArgumentException(
309                        "Trace identifier exceeds " + TRACE_ID_LENGTH + " characters: " + trace);
310            }
311            this.traceIdentifier = padTrace(trace == null ? "" : trace);
312            return this;
313        }
314
315        /**
316         * Sets the life cycle transaction sequence number.
317         *
318         * @param seq sequence number (0–9999)
319         * @return this builder
320         */
321        public Builder sequenceNumber(int seq) {
322            this.sequenceNumber = seq;
323            return this;
324        }
325
326        /**
327         * Sets the life cycle authentication token.
328         *
329         * @param token authentication token (0–9999)
330         * @return this builder
331         */
332        public Builder authToken(int token) {
333            this.authToken = token;
334            return this;
335        }
336
337        /**
338         * Builds the {@link LifeCycleId}.
339         *
340         * @return new LifeCycleId
341         */
342        public LifeCycleId build() {
343            return new LifeCycleId(this);
344        }
345
346        private static String padTrace(String s) {
347            if (s.length() == TRACE_ID_LENGTH) return s;
348            return String.format("%-" + TRACE_ID_LENGTH + "s", s);
349        }
350    }
351
352    @Override
353    public String toString() {
354        return "LifeCycleId{indicator=" + supportIndicator
355                + ", trace='" + traceIdentifier.trim() + "'"
356                + ", seq=" + sequenceNumber
357                + ", token=" + authToken + "}";
358    }
359}