delete()` convention; neither outcome is an error. * * - {@see self::save()} is insert-only: a non-null-id record MUST * raise {@see \RuntimeException}. See the design notes above for * the rationale. * * @package Affilun\Infrastructure\Persistence */ declare(strict_types=1); namespace Affilun\Infrastructure\Persistence; use RuntimeException; /** * Persistence port for `affilun_logs` rows. * * See the file-level docblock for the contract's scope, the record * type it exchanges, the insert-only nature of `save()`, and its * not-found / delete semantics. */ interface LogRepository { /** * Persist the given log record. * * This is an INSERT-ONLY operation. The schema's append-only * design (§9.2 omits `updated_at`; §9.4 defers retention to a * purge job) means there is no supported mutation path for an * existing row, so implementations MUST: * * - Accept only records whose id is null. The implementation * assigns the auto-increment primary key and returns a new * record carrying the assigned id. * * - Raise {@see RuntimeException} when the supplied record * already carries an id. The append-only schema has no way * to honor the update, and silently inserting a duplicate * (or silently ignoring the call) would mislead callers * about the fate of their data. * * @param LogRecord $record Record to persist. Must carry a null id. * * @return LogRecord The post-insert record, always carrying a * non-null id. * * @throws RuntimeException When the underlying driver reports an * error, or when the supplied record * already carries a non-null id. */ public function save( LogRecord $record ): LogRecord; /** * Look up a record by its auto-increment primary key. * * Returns null when no row matches. Non-positive ids are a * programming error and implementations MAY raise * {@see \InvalidArgumentException}; callers that only have an * optional id in hand should filter before calling. * * @param int $id Primary key to look up. * * @throws RuntimeException When the underlying driver reports an * error. */ public function findById( int $id ): ?LogRecord; /** * Delete the row identified by the given id. * * Idempotent: returns true when a row was removed, false when no * row matched. Neither outcome is an error. * * Non-positive ids are a programming error and implementations * MAY raise {@see \InvalidArgumentException}. * * Bulk purge — removing every row older than a cutoff, or every * row on a given channel — is NOT part of this contract; that * operation belongs with the `cleanup_logs` purge job (§8.4 / * §9.4). This method is a single-row helper suitable for tests * and for targeted diagnostics cleanup. * * @param int $id Primary key of the row to delete. * * @throws RuntimeException When the underlying driver reports an * error. */ public function delete( int $id ): bool; /** * Total number of rows currently stored in `affilun_logs`. * * Used by diagnostics and integration tests to assert on the * pipeline outcome without enumerating rows. Implementations * SHOULD back this with a `SELECT COUNT(*)` query rather than * materializing every row. * * @throws RuntimeException When the underlying driver reports an * error. */ public function count(): int; /** * Look up a paginated, filtered slice of stored rows. * * Implements the `GET /affilun/v1/logs` query surface described in * `specifications/rest-api-spec.md` §12.2. The query VO carries * the optional column-level filter axis (`level`, `channel`, * `relatedProductId`, `relatedJobId`) and the pagination axis * (`page`, `perPage`); see {@see LogListQuery} for the exhaustive * validation rules and the deferred JSON-extraction filters * (`event_type`, `request_id`, `correlation_id`, `job_id`) tracked * for Phase 11 / 12. * * The returned slice is ordered newest-first * (`ORDER BY created_at DESC, id DESC`); spec §12.2 does not * expose a sort parameter, so the ordering is fixed at the * repository boundary. The total reported on the result envelope * reflects the filter set BEFORE pagination so REST consumers can * compute the page count without re-issuing the query. * * @param LogListQuery $query Validated list-query inputs. * * @throws RuntimeException When the underlying driver reports an * error or when a returned row cannot be * decoded into a {@see LogRecord}. */ public function findPaginated( LogListQuery $query ): LogListResult; /** * Group every stored row by PSR-3 level and return the per-level * counts. * * Implements the level-histogram half of the `GET /affilun/v1/logs/summary` * surface described in `specifications/rest-api-spec.md` §12.3. * Levels with zero occurrences are OMITTED from the returned map; * callers that need a fully-populated histogram (every PSR-3 * level with a numeric count) should default-fill against * {@see LogListQuery::VALID_LEVELS}. * * Implementations SHOULD back this with a single * `SELECT level, COUNT(*) ... GROUP BY level` query rather than * issuing one count per level. * * @return array Map of PSR-3 level => count. Levels * with zero occurrences are omitted. * * @throws RuntimeException When the underlying driver reports an * error. */ public function countByLevel(): array; /** * Return the most recent log entries, newest-first. * * Implements the recent-entries half of the * `GET /affilun/v1/logs/summary` surface described in * `specifications/rest-api-spec.md` §12.3. Ordering matches * {@see self::findPaginated()} (`ORDER BY created_at DESC, id DESC`) * so a "first page of recent entries" call site cannot disagree * with the histogram about which rows are most recent. * * `$limit` is bounded to the same 1..100 range as * {@see LogListQuery::MAX_PER_PAGE} so the summary endpoint * cannot be coerced into materializing the full table. * * @param int $limit Maximum number of entries to return (1..100). * * @return list * * @throws \InvalidArgumentException When `$limit` is out of range. * @throws RuntimeException When the underlying driver * reports an error or when a * returned row cannot be decoded * into a {@see LogRecord}. */ public function findRecent( int $limit ): array; }