Specification — ZCL_HR_INFOTYPE_READER

Status: Draft v1.0 Related: ../CLAUDE.md, infotype_reading_pattern.md, exception_class_spec.md


1. Purpose

Provide a single reusable entry point for any ABAP program that needs to read multiple PA infotypes for one personnel number over a date range and obtain the data as a time-sliced consolidated table — one row per interval of unchanged field values across all requested infotypes, with each requested infotype's records nested per row as a sub-table.

The class replaces ad-hoc sequences of CL_HRPA_READ_INFOTYPE=>READ_INFOTYPE calls scattered across reports and BAdIs, centralizing:

  • Primary vs Secondary infotype dispatch
  • READ vs READ_WIDE selection
  • Buffering across repeated reads (two-level cache)
  • Authority handling (MISSING_AUTH as warning, no_auth_check opt-out)
  • Lock-status handling (SPRPS)
  • Time-slicing of heterogeneous infotype validity intervals into a single aggregated timeline
  • Structured, typed exception handling and structured message collection

The internal reading mechanics are documented separately in infotype_reading_pattern.md. This document focuses on the public API and the aggregation behavior.

2. Naming and location

ObjectName
ClassZCL_HR_INFOTYPE_READER
Exception classZCX_HR_INFOTYPE_READER

3. Public types

Published as public types of the class so callers can import them without duplication.

" -- Request: which infotypes to read, with optional subtype filter and mandatory flag
TYPES:
  BEGIN OF ty_infotype_req,
    infty     TYPE infty,                         " e.g. '0001'
    subty     TYPE subty,                         " SPACE = all subtypes
    mandatory TYPE abap_bool,                     " 'X' = empty result is a hard error
  END OF ty_infotype_req,
  ty_infotype_req_t TYPE STANDARD TABLE OF ty_infotype_req WITH DEFAULT KEY.

" -- Returned message (for caller-side logging; NOT raised)
TYPES:
  BEGIN OF ty_message,
    msgty TYPE symsgty,                           " 'S' / 'I' / 'W' / 'E' — typically 'W'
    msgid TYPE symsgid,
    msgno TYPE symsgno,
    msgv1 TYPE symsgv,
    msgv2 TYPE symsgv,
    msgv3 TYPE symsgv,
    msgv4 TYPE symsgv,
    text  TYPE string,                            " pre-formatted human-readable text (convenience)
    pernr TYPE persno,                            " context
    infty TYPE infty,                             " context (SPACE if not infotype-specific)
  END OF ty_message,
  ty_message_t TYPE STANDARD TABLE OF ty_message WITH DEFAULT KEY.

" -- Execution statistics (diagnostic output)
TYPES:
  BEGIN OF ty_stats,
    infotypes_requested TYPE i,
    infotypes_read      TYPE i,                   " actually hit DB (excludes buffer hits)
    records_read        TYPE i,                   " total PRELP rows fetched from DB
    buffer_hits         TYPE i,
    buffer_misses       TYPE i,
    intervals_generated TYPE i,                   " before collapse
    intervals_final     TYPE i,                   " after collapse
    runtime_ms          TYPE i,
  END OF ty_stats.

4. Public constants

CONSTANTS:
  " Lock-status filter (maps onto IF_HRPA_READ_INFOTYPE SPRPS values)
  BEGIN OF c_sprps,
    unlocked TYPE char1 VALUE '0',                " active records only (default)
    locked   TYPE char1 VALUE '1',                " locked records only
    all      TYPE char1 VALUE '2',                " both
  END OF c_sprps,

  " Read mode
  BEGIN OF c_read_mode,
    buffer_first TYPE char1 VALUE 'B',            " default: cache, fall back to DB
    database     TYPE char1 VALUE 'D',            " bypass cache, always hit DB (still populates cache)
    buffer_only  TYPE char1 VALUE 'M',            " cache only, never hit DB; miss -> empty
  END OF c_read_mode.

Note that c_sprps values mirror the framework's IF_HRPA_READ_INFOTYPE=>UNLOCKED / LOCKED / ALL constants. Exposing them as a local enum avoids cluttering every caller with IF_HRPA_* references.

5. Factory

The factory is the only way to obtain an instance — the class is CREATE PRIVATE, so callers must go through create( … ).

CLASS-METHODS create
  IMPORTING
    iv_enable_buffer     TYPE abap_bool DEFAULT abap_true
    iv_no_auth_check     TYPE abap_bool DEFAULT abap_false
    iv_sprps             TYPE char1     DEFAULT c_sprps-unlocked
    iv_read_mode         TYPE char1     DEFAULT c_read_mode-buffer_first
    iv_collapse_equal    TYPE abap_bool DEFAULT abap_true
  RETURNING
    VALUE(ro_reader)     TYPE REF TO zcl_hr_infotype_reader.
ParameterPurpose
iv_enable_bufferIf abap_false, every call re-reads from DB. Class-level meta cache still used.
iv_no_auth_checkPass-through to framework no_auth_check. Only set from contexts where auth is pre-established.
iv_sprpsLock-status filter. Default unlocked — exclude locked records.
iv_read_modeSee constants in §4.
iv_collapse_equalIf abap_true (default), adjacent time-sliced rows with byte-equal nested tables merge into one row with widened BEGDA..ENDDA. Turn off for debugging slice logic.

The factory never raises — all work that can fail is deferred to the read methods.

6. Main methods

The caller pre-declares a structured line type whose components correspond to the infotypes they intend to read, passes an empty internal table via CHANGING, and the method fills it.

METHODS read_aggregated_typed
  IMPORTING
    iv_pernr       TYPE persno
    iv_begda       TYPE begda
    iv_endda       TYPE endda
    it_infotypes   TYPE ty_infotype_req_t
  EXPORTING
    et_messages    TYPE ty_message_t
    es_stats       TYPE ty_stats
  CHANGING
    ct_result      TYPE STANDARD TABLE                 " caller-declared structured table
  RAISING
    zcx_hr_infotype_reader.

The line type of CT_RESULT must have at least these components:

ComponentTypeRequired
PERNRPERSNO (compatible)yes
BEGDABEGDAyes
ENDDAENDDAyes
T_IT<NNNN>STANDARD TABLE OF P<NNNN> or structurally compatibleone per entry in IT_INFOTYPES

Example caller line type:

TYPES:
  BEGIN OF ty_out_data,
    pernr    TYPE persno,
    begda    TYPE begda,
    endda    TYPE endda,
    t_it0000 TYPE STANDARD TABLE OF p0000 WITH DEFAULT KEY,
    t_it0001 TYPE STANDARD TABLE OF p0001 WITH DEFAULT KEY,
    t_it0002 TYPE STANDARD TABLE OF p0002 WITH DEFAULT KEY,
  END OF ty_out_data,
  ty_out_data_t TYPE STANDARD TABLE OF ty_out_data WITH DEFAULT KEY.

6.2 Dynamic mode (alternative)

The class builds the line type on the fly using RTTC and returns a data reference to a dynamically-typed structured table.

METHODS read_aggregated_dynamic
  IMPORTING
    iv_pernr       TYPE persno
    iv_begda       TYPE begda
    iv_endda       TYPE endda
    it_infotypes   TYPE ty_infotype_req_t
  EXPORTING
    er_result      TYPE REF TO data                    " dynamically built structured table
    ero_result_type TYPE REF TO cl_abap_tabledescr     " RTTI handle for caller introspection
    et_messages    TYPE ty_message_t
    es_stats       TYPE ty_stats
  RAISING
    zcx_hr_infotype_reader.

The built line type has components named exactly PERNR, BEGDA, ENDDA, T_IT<NNNN> where <NNNN> matches every distinct INFTY in IT_INFOTYPES (de-duplicated, sorted ascending). Each nested table is typed STANDARD TABLE OF P<NNNN>.

Callers access the result via field symbols:

DATA(lo_reader) = zcl_hr_infotype_reader=>create( ).
DATA lr_result       TYPE REF TO data.
DATA lro_result_type TYPE REF TO cl_abap_tabledescr.

lo_reader->read_aggregated_dynamic(
  EXPORTING
    iv_pernr     = '00000001'
    iv_begda     = '20260101'
    iv_endda     = '20261231'
    it_infotypes = VALUE #( ( infty = '0001' ) ( infty = '0002' ) )
  IMPORTING
    er_result       = lr_result
    ero_result_type = lro_result_type
  ).

FIELD-SYMBOLS: <lt_result> TYPE STANDARD TABLE.
ASSIGN lr_result->* TO <lt_result>.

LOOP AT <lt_result> ASSIGNING FIELD-SYMBOL(<ls_row>).
  ASSIGN COMPONENT 'T_IT0001' OF STRUCTURE <ls_row> TO FIELD-SYMBOL(<lt_it0001>).
  " ...
ENDLOOP.

6.3 When to use which mode — Design Decision

Design decision — typed mode is the default recommendation.

Typed mode gives compile-time field access, IDE completion, and the clearest call site, matching the pattern in your original example. Dynamic mode exists for the case where the infotype list is assembled at runtime (e.g. from a Customizing table or a selection screen), and declaring a matching static type is impractical.

The typed-mode validator in the class (see §8) enforces structural compatibility with a clear error if the caller's type drifts from the requested IT_INFOTYPES, so the two modes are interchangeable in semantics, only differing in who owns the line type.

6.4 Auto-expansion of linked infotypes

Before reading and before typed-mode validation, the class internally expands IT_INFOTYPES with the linked partner (primary ↔ secondary) of every requested infotype that has one, on the resolved MOLGA. The original entries are preserved (order, subtype filter, mandatory flag); the appended partner entries carry mandatory = abap_false and no subtype filter.

Consequences:

  • If the caller requests 0001 and T582V/T582W link it to 0003 on the employee's MOLGA, the effective request becomes { 0001, 0003 }, READ_WIDE fires once, and the final result contains both T_IT0001 and T_IT0003.
  • If the caller requests only the secondary (say 0003), the effective request becomes { 0003, 0001 }, READ_WIDE reads the primary, and the result contains T_IT0003 and T_IT0001.
  • Typed-mode callers therefore need T_IT<NNNN> components for any linked partner of the infotypes they request, or they will get STRUCTURE_MISMATCH. When in doubt, declare both sides of a known pair (0001/0003, 0008/0052, etc.) in the caller's line type.
  • Dynamic mode builds the line type from the expanded list automatically, so the caller receives T_IT<NNNN> components for the partners as well.

7. Buffer helpers

METHODS clear_buffer.                             " drop all instance-level PRELP cache entries
METHODS clear_buffer_for_pernr IMPORTING iv_pernr TYPE persno.
METHODS get_buffer_stats RETURNING VALUE(rs_stats) TYPE ty_stats.

The class-level meta cache (GT_META_CACHE) is not cleared by these methods — it is static per MOLGA and its content is never invalidated within a program run. A public class method CLEAR_META_CACHE is exposed for unit testing; production callers should not invoke it.

8. RTTI validation (typed mode)

At the start of READ_AGGREGATED_TYPED, after IT_INFOTYPES has been expanded with linked partners (§6.4), validate that the caller's CT_RESULT line type is compatible with the expanded infotype list. Validation is strict on missing components and lenient on extras:

  1. Describe the line type via CL_ABAP_STRUCTDESCR=>DESCRIBE_BY_DATA on a dummy line.
  2. Verify these mandatory components exist: PERNR, BEGDA, ENDDA (existence check by name).
  3. For every INFTY in the expanded request (deduplicated), verify a component named T_IT<NNNN> exists and has kind = KIND_TABLE. Structural compatibility of the nested line type is not checked up-front — actual compatibility with P<NNNN> is enforced at serve time by CL_HR_PNNNN_TYPE_CAST=>PRELP_TO_PNNNN_TAB, which will raise CX_HRPA_VIOLATED_ASSERTION if incompatible. That assertion is re-raised as CONVERSION_FAILURE (§10).
  4. Extra components in the line type (neither required meta components nor any T_IT<NNNN> for a requested or auto-expanded infotype) trigger a warning in ET_MESSAGES but do not fail the call.
  5. Any missing mandatory component or any missing T_IT<NNNN> (for either a requested or an auto-expanded infotype) raises ZCX_HR_INFOTYPE_READER with textid = STRUCTURE_MISMATCH and exc_no = 4. No read is performed.

9. Aggregation algorithm — time-slicing

After all requested infotypes are read (via the pattern in infotype_reading_pattern.md), the class builds the output table as follows.

9.1 Input to the aggregator

  • V_BEGDA, V_ENDDA — the caller's window.
  • For each infotype I_k in IT_INFOTYPES: a list of records R_{k,1} .. R_{k,n_k}, each with BEGDA, ENDDA and payload fields. Records are those returned by the framework as overlapping the window (see pattern doc §5), i.e. every record with R.BEGDA ≤ V_ENDDA AND R.ENDDA ≥ V_BEGDA.

9.2 Step 1 — collect boundary dates

Initialize boundaries = { V_BEGDA, V_ENDDA + 1 }.

For every record of every requested infotype:

  • If R.BEGDA > V_BEGDA, add R.BEGDA to boundaries.
  • If R.ENDDA < V_ENDDA, add R.ENDDA + 1 to boundaries.

Dates clipped below V_BEGDA or above V_ENDDA + 1 are discarded (they would fall outside the window).

Sort and deduplicate — this yields B_1 < B_2 < ... < B_m with B_1 = V_BEGDA and B_m = V_ENDDA + 1.

9.3 Step 2 — form intervals

Intervals are [B_j, B_{j+1} - 1] for j = 1 .. m - 1. Each interval is non-empty and non-overlapping with the others, and their union is exactly [V_BEGDA, V_ENDDA].

If no records exist for any infotype, the only boundaries are { V_BEGDA, V_ENDDA + 1 } and a single interval [V_BEGDA, V_ENDDA] is produced with all nested tables empty.

9.4 Step 3 — snapshot each interval

For each interval [I_BEG, I_END] and each requested infotype I_k:

  • Collect every record of I_k that is active in the interval: R.BEGDA ≤ I_END AND R.ENDDA ≥ I_BEG.
  • Place those records, as-is, into the interval's nested table T_IT<NNNN>.

An output row is then constructed:

FieldValue
PERNRIV_PERNR
BEGDAI_BEG
ENDDAI_END
T_IT<NNNN>records of infotype <NNNN> active in [I_BEG, I_END]

Design decision — records in nested tables retain their original BEGDA/ENDDA.

Your requirement text says nested tables should be "without BEGDA/ENDDA" because those are lifted to the outer row. Your code example, however, uses STANDARD TABLE OF P0001 which structurally contains BEGDA/ENDDA. The simplest design that honors both is:

  • The outer row's BEGDA/ENDDA is the canonical interval for that snapshot.
  • The nested records' BEGDA/ENDDA hold the original record's validity, preserving traceability to the source row in PA<NNNN>.

This costs nothing in memory or correctness and is a common debugging lifesaver. If a caller genuinely wants stripped-date nested tables, they can declare their own flat structure (e.g. TY_P0001_FLAT without BEGDA/ENDDA/SEQNR) as the nested line type, and MOVE-CORRESPONDING will do the right thing.

If you want the stricter "nested records must have no BEGDA/ENDDA" contract instead, flip a factory flag iv_strip_nested_dates. I haven't added it to the signature above because the default is cleaner without it — raise it in review and I'll add.

9.5 Step 4 — collapse equal adjacent rows (optional)

If IV_COLLAPSE_EQUAL = abap_true (factory default):

Iterate the output in ascending BEGDA order. For each pair of consecutive rows R_j, R_{j+1} where R_j.ENDDA + 1 = R_{j+1}.BEGDA and every T_IT<NNNN> table in R_j is byte-equal to the corresponding table in R_{j+1} (comparison by serialized content, not by reference): merge them — set R_j.ENDDA = R_{j+1}.ENDDA and delete R_{j+1}.

Repeat until no more merges are possible. In practice one pass suffices because once a merge happens the merged row's content has not changed, so further merge candidates are tested against the merged row.

Design decision — "equal" is content-equal, not source-identical.

Two snapshots are considered equal when every payload field in every record of every nested table matches. This includes record-level BEGDA/ENDDA if they are part of the nested structure — which means that when the nested rows' own validity fields differ between two otherwise-equal intervals (e.g. because the source record switched but kept all payload fields), the rows will NOT collapse. This is usually the right behavior for audit but can be surprising. Callers who want collapse on payload-only should declare a stripped nested type (see §9.4 design note).

9.6 Example

Employee 00000001, window 01.01.2026 .. 31.12.2026, requested [0001, 0002].

  • PA0001 data:
    • [01.01.2026 .. 30.06.2026] WERKS='PL01' ORGEH='10000001'
    • [01.07.2026 .. 31.12.2026] WERKS='PL01' ORGEH='10000002'
  • PA0002 data:
    • [01.01.2026 .. 31.03.2026] NACHN='Kowalski' ANRED='Mr'
    • [01.04.2026 .. 31.12.2026] NACHN='Nowak' ANRED='Mr'

Boundaries: { 01.01.2026, 01.04.2026, 01.07.2026, 01.01.2027 }

Intervals: [01.01..31.03], [01.04..30.06], [01.07..31.12]

BEGDAENDDAT_IT0001 (payload)T_IT0002 (payload)
01.01.202631.03.2026ORGEH=10000001NACHN=Kowalski
01.04.202630.06.2026ORGEH=10000001NACHN=Nowak
01.07.202631.12.2026ORGEH=10000002NACHN=Nowak

No adjacent intervals collapse (every pair differs in at least one nested table).

10. Warnings vs exceptions

Warnings are always appended to ET_MESSAGES with MSGTY = 'W' and never raise. Exceptions always raise ZCX_HR_INFOTYPE_READER with a defined textid and exc_no. The complete classification is in exception_class_spec.md §6.

The key rule: anything per-infotype is a warning (skip that infotype, continue). Anything per-call is an exception (abort the whole call). MANDATORY_INFOTYPE_EMPTY is the only exception to this rule — it promotes a per-infotype condition to a hard error when the caller flagged the infotype as mandatory.

11. Example — typed mode

REPORT zhr_test_reader.

TYPES:
  BEGIN OF ty_out_data,
    pernr    TYPE persno,
    begda    TYPE begda,
    endda    TYPE endda,
    t_it0000 TYPE STANDARD TABLE OF p0000 WITH DEFAULT KEY,
    t_it0001 TYPE STANDARD TABLE OF p0001 WITH DEFAULT KEY,
    t_it0002 TYPE STANDARD TABLE OF p0002 WITH DEFAULT KEY,
  END OF ty_out_data,
  ty_out_data_t TYPE STANDARD TABLE OF ty_out_data WITH DEFAULT KEY.

DATA lt_result TYPE ty_out_data_t.
DATA lt_msg    TYPE zcl_hr_infotype_reader=>ty_message_t.
DATA ls_stats  TYPE zcl_hr_infotype_reader=>ty_stats.

TRY.
    DATA(lo_reader) = zcl_hr_infotype_reader=>create( ).

    lo_reader->read_aggregated_typed(
      EXPORTING
        iv_pernr     = '00000001'
        iv_begda     = '20260101'
        iv_endda     = '20261231'
        it_infotypes = VALUE #(
          ( infty = '0000' mandatory = abap_true )
          ( infty = '0001' mandatory = abap_true )
          ( infty = '0002' )
        )
      IMPORTING
        et_messages = lt_msg
        es_stats    = ls_stats
      CHANGING
        ct_result   = lt_result
    ).

    " consume lt_result, log lt_msg, report ls_stats

  CATCH zcx_hr_infotype_reader INTO DATA(lx).
    MESSAGE lx TYPE 'E'.
ENDTRY.

12. Test plan (minimum coverage)

#ScenarioExpected
1Single infotype, single unchanged recordOne output row, window-wide
2Single infotype, two adjacent records with different payloadsTwo rows, no collapse
3Two infotypes with overlapping but non-aligned splitsThree rows (per §9.6 example)
4Primary with linked secondary (e.g. 0001/0003 on MOLGA 46)READ_WIDE called once; both cached
5Secondary requested aloneOwning primary resolved; READ_WIDE on primary; secondary served from cache
6Invalid PERNR (no PA0001 overlap)ZCX_HR_INFOTYPE_READER with exc_no = 1
7BEGDA > ENDDAZCX_HR_INFOTYPE_READER with exc_no = 2
8Empty IT_INFOTYPESZCX_HR_INFOTYPE_READER with exc_no = 3
9Caller's structure missing a required T_IT<NNNN>ZCX_HR_INFOTYPE_READER with exc_no = 4
10MISSING_AUTH on one infotypeWarning in ET_MESSAGES, nested table empty, other infotypes populated
11Mandatory infotype returns emptyZCX_HR_INFOTYPE_READER with exc_no = 7
12Second call with same PERNR+infotypeBuffer hit, no DB read, ES_STATS-BUFFER_HITS = 1
13IV_COLLAPSE_EQUAL = false vs trueSame data, different row counts when flat segments exist
14SPRPS = LOCKEDOnly locked records present in output
15Subtype filter on 0041 (Date Specifications)Only rows with matching SUBTY in T_IT0041
16Customer infotype with blank T777D-INFKNWarning, falls back to READ as primary
17Record with BEGDA = '00010101' / ENDDA = '99991231'Clipped intervals use V_BEGDA and V_ENDDA

13. What is explicitly NOT in this class

See CLAUDE.md §"Out of scope". Highlights: no writes, no cluster reads, no OM infotypes, no payroll results, no multi-PERNR, no time evaluation.

14. Open questions (flagged for review)

  1. Nested date stripping — §9.4 leaves BEGDA/ENDDA in the nested records. Should the factory expose iv_strip_nested_dates as a knob? Default answer: no, callers who need it declare a flat nested type.
  2. MOLGA change within window — §7 of the pattern doc uses MOLGA from the BEGDA-valid PA0001. If the employee has a country transfer during the window, the metadata resolution is pinned. Acceptable? Alternative: re-resolve per interval after the first pass.
  3. VINFT-based linkage cardinality — the current resolver assumes at most one linked secondary per primary (SELECT SINGLE). If a view has multiple secondaries, only the first (by DB read order) wins. Confirm this is acceptable for our infotype set, or change to SELECT with collection.
  4. Auto-expansion opt-out — §6.4 unconditionally appends linked partners to IT_INFOTYPES. A caller who wants only the requested side (e.g. requests 0001, does not want T_IT0003 in the result for MOLGA 46) currently has no way to opt out. Default answer: accept the widening because the physical read already fetches both via READ_WIDE; revisit if a concrete caller objects.
  5. Warning message numbers — the implementation appends every runtime warning (missing_auth, orphan secondary, unknown INFKN, extra structure components) with msgno = '010', which collides with the READER_NOT_AVAILABLE text in message class ZHR_INFOTYPE_READER. Allocate dedicated warning message numbers (proposed W01..W09) and update append_warning accordingly.
Built with LogoFlowershow