Caso de estudio · ABAP Cloud

Gestión de Incidencias con RAP

Aplicación Fiori para registrar incidencias, asignarlas a un responsable y seguir su historial de estados. Proyecto final del Máster SAP ABAP Cloud de Avanzado a Experto, desarrollado en SAP BTP ABAP Environment y versionado con abapGit.

Ver el código en GitHub Decisiones técnicas

El problema

Un equipo de soporte necesita una herramienta sencilla dentro de SAP para dar de alta incidencias, repartirlas y saber en todo momento en qué estado están y quién las ha movido. Las reglas que pedía el enunciado:

  • Cada incidencia tiene título, descripción, prioridad, fechas y responsable.
  • Al crearla, el número y el estado inicial se asignan solos.
  • El estado cambia mediante una acción, con un comentario, y cada cambio queda en un historial.
  • Solo el responsable o quien la creó puede cambiar el estado.
  • Una incidencia pendiente no puede cerrarse directamente.
  • Solo se pueden borrar incidencias abiertas.

Ficha técnica

Plataforma
SAP BTP ABAP Environment (ABAP Cloud)
Paquete
ZABAP_INCT_AGG
Modelo
RAP gestionado, strict ( 2 ), draft, numeración UUID gestionada
Entidades
Incidencia (raíz) e Historial (hija, por composición)
Servicio
OData V4 UI, app Fiori Elements List Report + Object Page
Objetos
4 tablas + 2 draft, 2 dominios, 7 vistas CDS, 2 BDEF, 4 clases, metadata extensions, service definition y binding
Capas del Business Object de incidencias
Arquitectura por capas. La interfaz se genera a partir de las anotaciones de las metadata extensions.

Estados y reglas de transición

El estado nunca se edita a mano: el campo es de solo lectura y solo cambia a través de la acción changeStatus, que valida las reglas y escribe el historial.

Estados abiertos (OP, IP, PE) y cerrados (CO, CL, CN) con sus reglas

Decisiones técnicas

1. Definición de comportamiento: qué hace el framework y qué hago yo

Con RAP gestionado, el framework se encarga de crear, actualizar, borrar, bloquear y guardar. Mi código solo implementa lo que es lógica de negocio. La BDEF declara qué campos son de solo lectura, qué validaciones se lanzan y cuándo.

managed implementation in class zbp_r_dt_inct_agg unique;
strict ( 2 );
with draft;

define behavior for zr_dt_inct_agg alias Incident
persistent table zdt_inct_agg
draft table zdt_d_inct_agg
lock master
total etag LastChangedAt
etag master LocalLastChangedAt
authorization master ( global, instance )
{
  create;
  update;
  delete;

  association _History { create ( features : instance ); with draft; }

  field ( readonly ) IncUUID, IncidentID, Status, ChangedDate,
                     LocalCreatedBy, LocalCreatedAt, LocalLastChangedBy,
                     LocalLastChangedAt, LastChangedAt;
  field ( numbering : managed ) IncUUID;
  field ( mandatory ) Title, Description, Priority;

  action ( features : instance ) changeStatus
    parameter zdd_change_status_param_agg result [1] $self;

  side effects { action changeStatus affects $self; }

  internal action setHistory;

  determination setDefaultHistory on save   { create; }
  determination setDefaultValues  on modify { create; }

  validation validateMandatoryFields on save { create; update; }
  validation validateDates           on save { field CreationDate, ChangedDate; }
  validation validateDeleteStatus    on save { delete; }
  ...
}

Extracto sin comentarios; las acciones de draft, el mapping y la entidad de historial están en el repositorio.

2. Botón deshabilitado en lugar de mensaje de error

Si una incidencia está cerrada, no tiene sentido dejar pulsar "Cambiar estado" para luego mostrar un error. Con features : instance el botón aparece deshabilitado. El caso delicado es el de una incidencia nueva que todavía es solo un borrador: no existe en la tabla activa y tampoco debe poder cambiar de estado. Para distinguirla, se comprueba si el UUID del borrador ya tiene registro activo:

result = VALUE #( FOR incident IN incidents
  LET is_closed = xsdbool(    incident-Status = 'CO'
                           OR incident-Status = 'CL'
                           OR incident-Status = 'CN' )
      is_new    = xsdbool( incident-%is_draft = if_abap_behv=>mk-on
                           AND NOT line_exists( lt_active[ inc_uuid = incident-IncUUID ] ) )
  IN
  ( %tky                 = incident-%tky
    %action-changeStatus = COND #( WHEN is_closed = abap_true OR is_new = abap_true
                                   THEN if_abap_behv=>fc-o-disabled
                                   ELSE if_abap_behv=>fc-o-enabled ) ) ).

3. La acción changeStatus: validar, actualizar y dejar rastro

La acción recibe el nuevo estado y un comentario a través de una entidad abstracta. Para cada incidencia aplica las tres reglas de negocio; si alguna falla, la incidencia va a FAILED, el mensaje a REPORTED y se pasa a la siguiente. Si todo es correcto, actualiza el estado con EML y crea la línea de historial por la composición:

" Regla de negocio: de Pendiente no se pasa directamente a Completada o Cerrada
IF incident-Status = 'PE'
  AND ( key_entry-%param-Status = 'CO' OR key_entry-%param-Status = 'CL' ).
  APPEND VALUE #( %tky = incident-%tky ) TO failed-incident.
  APPEND VALUE #( %tky        = incident-%tky
                  %state_area = 'VALIDATE_STATUS_CHANGE'
                  %msg        = new_message_with_text(
                                  severity = if_abap_behv_message=>severity-error
                                  text     = 'Cannot change status from Pending to Completed or Closed' ) )
    TO reported-incident.
  CONTINUE.
ENDIF.

MODIFY ENTITIES OF zr_dt_inct_agg IN LOCAL MODE
  ENTITY Incident
    CREATE BY \_History
    FIELDS ( HisID PreviousStatus NewStatus Text )
    WITH VALUE #( ( %tky    = incident-%tky
                    %target = VALUE #( ( %cid           = |HIST_CS_{ incident-IncUUID }|
                                         HisID          = next_his_id
                                         PreviousStatus = incident-Status
                                         NewStatus      = key_entry-%param-Status
                                         Text           = key_entry-%param-Text ) ) ) ).

4. Validaciones que señalan el campo

Las validaciones de campos obligatorios rellenan %element además del mensaje, así que Fiori marca en rojo el campo concreto. Cada validación usa su propio %state_area para que los mensajes desaparezcan cuando el usuario corrige el dato.

5. Valores por defecto al crear

La determinación setDefaultValues asigna el número correlativo, el estado OP y las fechas con cl_abap_context_info=>get_system_date( ) (en ABAP Cloud no se usa sy-datum). Al guardar por primera vez, setDefaultHistory crea la primera línea del historial.

Qué cambiaría en un proyecto productivo. El número de incidencia se calcula con MAX + 1, válido para el ejercicio pero con riesgo de duplicados si dos usuarios crean a la vez. En productivo usaría un objeto de rango de números (cl_numberrange_runtime) en early numbering. También movería los textos de los mensajes a una clase de mensajes para poder traducirlos, y añadiría tests de EML con cl_botd_txbufdbl_bo_test_env.

6. Interfaz sin escribir interfaz

La app Fiori no tiene ni una línea de JavaScript. Las metadata extensions definen columnas, filtros, facetas del detalle y los botones de acción (#FOR_ACTION con dataAction: 'changeStatus'), y los value helps de estado y prioridad usan búsqueda difusa con umbral 0.8.

Lo que aprendí

  • En RAP, la mayor parte del trabajo está en decidir bien qué va en la BDEF y qué en el código. Una BDEF bien pensada ahorra métodos enteros.
  • Los borradores (draft) cambian cómo hay que razonar: cada lectura puede devolver una instancia activa o un borrador, y hay que contemplar ambos casos.
  • abapGit desde el primer día: el historial de commits es también la documentación de cómo se construyó.

¿Quieres montar algo parecido? El generador de BO RAP te deja el esqueleto listo con draft, validaciones y una acción con parámetro, y la plantilla RAP se descarga gratis.