# Server-Side Shift Closure Implementation

**Stack:** jPOS 2.1.x · Hibernate/JPA · MySQL · Flyway migrations

---

## How Shift Closure fits the existing architecture

The terminal sends **MTI `0520`** (Settlement Advice). The server routes `0520` exclusively to `SHIFT_CLOSURE`, while `0500` (Settlement Request) continues to route to `SETTLEMENT` unchanged.

The shift number arrives in **DE-47** as JSON `{"shiftNo":"003"}` — the same field already used for `origMti`, `pin`, etc.

---

## How Shift Closure differs from Settlement

| | Shift Closure (`0520`) | Settlement (`0500`) |
|---|---|---|
| MTI | `0520` (Settlement Advice) | `0500` (Settlement Request) |
| Scope | Closes transactions for **one specific shift** (filtered by `shift_no`) | Closes **all OPEN** transactions for the terminal |
| Batch number | **Not** incremented | Incremented |
| Audit record | Saves a `pos_shift_closure` row | No equivalent |
| Response | `0530` / DE-39 `"00"` | `0510` / DE-39 `"00"` |

---

## Message Flow (end-to-end)

```
Terminal                          jPOS Server
  │                                   │
  │── 0520 / DE-3:960000 ────────────▶│
  │   DE-11: STAN                     │ getTransactionType()
  │   DE-41: TID                      │   → SHIFT_CLOSURE
  │   DE-42: MID                      │
  │   DE-47: {"shiftNo":"003"}        │ ShiftClosureTransactionProcessor
  │   DE-62: batchNo                  │   parse shiftNo from DE-47
  │                                   │   findOpenByShiftNo(bTid, bMid, "003")
  │                                   │   calculate DR / CR / NET totals
  │                                   │   markAsShiftClosed(ids)
  │                                   │   save PosShiftClosure record
  │                                   │
  │◀─ 0530 / DE-39:"00" ─────────────│
```

---

## Files Modified

### 1. `AdditionalData.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/data_element/AdditionalData.java`

Added `shiftNo` field:
```java
@SerializedName("shiftNo")
String shiftNo;
```

> **Note:** `@AllArgsConstructor` is present on this class. Any existing call sites using the all-args constructor must add a trailing `null` argument for `shiftNo`.

---

### 2. `TxnType.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/constant/TxnType.java`

Added:
```java
SHIFT_CLOSURE("SHIFT_CLOSURE", false, 0),
```

---

### 3. `PaymentUtil.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/PaymentUtil.java`

Added the type list:
```java
public static List<TxnType> SHIFT_CLOSURE_TXN_TYPES = Arrays.asList(TxnType.SHIFT_CLOSURE);
```

In `getTransactionType()`, `0500` and `0520` are now routed separately — no processing code inspection needed:
```java
} else if (MessageTypeConstants.SETTLEMENT_REQUEST.equals(mti)) {   // 0500
    return TxnType.SETTLEMENT;
} else if (MessageTypeConstants.SETTLEMENT_ADVICE.equals(mti)) {    // 0520
    return TxnType.SHIFT_CLOSURE;
}
```

---

### 4. `TransactionProcessorFactory.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/factory/TransactionProcessorFactory.java`

Added routing before the Settlement check:
```java
if (PaymentUtil.SHIFT_CLOSURE_TXN_TYPES.contains(txnType)) {
    return new ShiftClosureTransactionProcessor(transactionAuxUtils, lifeCycle, reversalService, txnType);
} else if (PaymentUtil.SETTLEMENT_TXN_TYPES.contains(txnType)) {
    ...
}
```

---

### 5. `PosTransaction.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/db/entity/PosTransaction.java`

Added column:
```java
@Column(name = "shift_no", length = 3)
private String shiftNo;
```

---

### 6. `PosTempTransaction.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/db/entity/PosTempTransaction.java`

Added column (same as above) so `shiftNo` flows through `copyTransactionFields()` into `PosTransaction` and `PosFailedTransaction` on transaction completion.

---

### 7. `PosFailedTransaction.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/db/entity/PosFailedTransaction.java`

Added column (same as above) for parity.

---

### 8. `TransactionAuxUtils.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/service/TransactionAuxUtils.java`

**In `createTempTransactionFromISO()`** — inside the existing DE-47 parsing block:
```java
if (additionalData.getShiftNo() != null) {
    tempTxn.setShiftNo(additionalData.getShiftNo());
}
```

**In `copyTransactionFields(PosTempTransaction, Object)`** — added `.setShiftNo(source.getShiftNo())` to both the `PosTransaction` and `PosFailedTransaction` copy branches.

**In `copyTransactionFields(PosTransaction, PosFailedTransaction)`** — added `.setShiftNo(source.getShiftNo())`.

---

### 9. `PosTransactionRepository.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/db/repository/PosTransactionRepository.java`

Added to interface:
```java
List<PosTransaction> findOpenByShiftNo(String bTid, String bMid, String shiftNo);
```

---

### 10. `PosTransactionRepositoryImpl.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/db/repository/PosTransactionRepositoryImpl.java`

Implemented `findOpenByShiftNo()`:
```java
public List<PosTransaction> findOpenByShiftNo(String bTid, String bMid, String shiftNo) {
    // SELECT t FROM PosTransaction t
    // WHERE t.bTid = :bTid AND t.bMid = :bMid
    //   AND t.closureStatus = OPEN AND t.shiftNo = :shiftNo
    // ORDER BY t.createdDateTime ASC
}
```

---

## New Files Created

### A. `ShiftClosureTransactionProcessor.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/processor/ShiftClosureTransactionProcessor.java`

Extends `ManagementTransactionProcessor` (same as `SettlementTransactionProcessor`).

```
processTransaction():
  1. Parse shiftNo from DE-47 JSON via AdditionalData.getShiftNo()
  2. Reject with MIDDLEWARE_SYSTEM_ERROR if shiftNo is null or blank
  3. For each AcquirerTerminal of the POS terminal:
       openTxns = findOpenByShiftNo(bTid, bMid, shiftNo)
  4. markAsShiftClosed(ids)   ← reuses existing repository method
  5. Calculate totals from openTxns:
       DR count/amount  — TxnType.netDirection > 0 (Sale, AuthComplete)
       CR count/amount  — TxnType.netDirection < 0 (Refund, Void)
       NET amount       — drAmount - crAmount
  6. Save PosShiftClosure record (shiftNo, DR/CR/NET totals, batchNo, terminal IDs)
  7. Return built response

postProcess():
  clone jposRequest
  setResponseMTI()          → 0520 becomes 0530
  set DE-39 = "00"
  unset DE-56, 64
  return response
```

---

### B. `PosShiftClosure.java` (JPA Entity)
**Path:** `jpos/src/main/java/org/jpos/tcpay/db/entity/PosShiftClosure.java`

```java
@Entity
@Table(name = "pos_shift_closure")
public class PosShiftClosure {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "s_tid",    length = 8)              private String sTid;
    @Column(name = "s_mid",    length = 15)             private String sMid;
    @Column(name = "b_tid",    length = 8)              private String bTid;
    @Column(name = "b_mid",    length = 15)             private String bMid;
    @Column(name = "shift_no", length = 3)              private String shiftNo;
    @Column(name = "batch_no", length = 6)              private String batchNo;
    @Column(name = "txn_count")                         private Integer txnCount;
    @Column(name = "dr_count")                          private Integer drCount;
    @Column(name = "dr_amount", precision=12, scale=2)  private BigDecimal drAmount;
    @Column(name = "cr_count")                          private Integer crCount;
    @Column(name = "cr_amount", precision=12, scale=2)  private BigDecimal crAmount;
    @Column(name = "net_amount", precision=12, scale=2) private BigDecimal netAmount;
    @Column(name = "response_code", length = 2)         private String responseCode;
    @Column(name = "closed_at", nullable = false)        private LocalDateTime closedAt;
}
```

---

### C. `PosShiftClosureRepositoryImpl.java`
**Path:** `jpos/src/main/java/org/jpos/tcpay/db/repository/PosShiftClosureRepositoryImpl.java`

Methods:
- `save(PosShiftClosure)` — persist the closure record

---

### D. DB Migration: `011_add_shift_no_to_pos_transaction.sql`
**Path:** `jpos/src/main/resources/db/migration/011_add_shift_no_to_pos_transaction.sql`

```sql
ALTER TABLE pos_transaction      ADD COLUMN shift_no VARCHAR(3) NULL;
ALTER TABLE pos_temp_transaction ADD COLUMN shift_no VARCHAR(3) NULL;
ALTER TABLE pos_failed_transaction ADD COLUMN shift_no VARCHAR(3) NULL;

CREATE INDEX idx_transaction_shift_no
    ON pos_transaction (b_tid, b_mid, shift_no, closure_status);
```

---

### E. DB Migration: `012_create_pos_shift_closure_table.sql`
**Path:** `jpos/src/main/resources/db/migration/012_create_pos_shift_closure_table.sql`

```sql
CREATE TABLE pos_shift_closure (
    id            BIGINT        AUTO_INCREMENT PRIMARY KEY,
    s_tid         VARCHAR(8),
    s_mid         VARCHAR(15),
    b_tid         VARCHAR(8),
    b_mid         VARCHAR(15),
    shift_no      VARCHAR(3)    NOT NULL,
    batch_no      VARCHAR(6),
    txn_count     INT           DEFAULT 0,
    dr_count      INT           DEFAULT 0,
    dr_amount     DECIMAL(12,2) DEFAULT 0,
    cr_count      INT           DEFAULT 0,
    cr_amount     DECIMAL(12,2) DEFAULT 0,
    net_amount    DECIMAL(12,2) DEFAULT 0,
    response_code VARCHAR(2),
    closed_at     DATETIME      NOT NULL,
    INDEX idx_shift_closure_stid_shift (s_tid, shift_no),
    INDEX idx_shift_closure_btid_shift (b_tid, b_mid, shift_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Verification Checklist

1. Send `0520` / proc-code `960000` with DE-47 `{"shiftNo":"003"}` — server responds `0530 / 00`
2. `pos_transaction` rows tagged with `shift_no = '003'` have `closure_status = SHIFT_CLOSED` after closure
3. `pos_shift_closure` table has one row with correct DR / CR / NET counts and amounts
4. Send `0500` / proc-code `920000` (existing Settlement) — still works unchanged
5. Send `0520` with missing DE-47 or missing `shiftNo` — server returns a non-`00` response code
6. Run existing test suite — all tests pass
