Private Docs

Adding a New Step — End-to-End

เพิ่ม step ใหม่เข้า Lego Engine ทั้งฝั่ง backend (Handler + StepCatalog + FlowStep + DI) และ frontend (component + STEP_REGISTRY) โดยยึด stepType เป็น contract

อัปเดต: 2026-06-04

การเพิ่ม step ใหม่แตะ ทั้ง 2 ฝั่ง และทั้งสองฝั่งผูกกันด้วยสิ่งเดียว: stepType (string)

stepType คือ contract

StepCatalogStepTypeCode
Backend HandlerStepType
FE STEP_REGISTRYkey

'AddressVerificationStep' — string เดียวกันทั้ง 3 ที่

ฝั่ง Backend (4 ขั้น)

ใช้ ExampleAddressVerificationStepHandler เป็น template (เป็น reference ไม่ได้ register จริง)

1) เขียน Handler (IFlowStepHandler)

public sealed class AddressVerificationStepHandler : IFlowStepHandler
{
    // ตรงเป๊ะกับ StepCatalog.StepTypeCode และ FE component key
    public string StepType => "AddressVerificationStep";

    public Task<FlowStepExecutionResult> ExecuteAsync(FlowStepExecutionContext ctx, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(ctx.InputJson))
            return Fail("Step.NoInput", "กรุณากรอกที่อยู่บริษัท");

        // parse: รองรับทั้ง { stepData: {...} } และ flat {...}
        // อ่านค่า "ก่อน" doc ถูก dispose (กัน ObjectDisposedException)
        using var doc = JsonDocument.Parse(ctx.InputJson);
        var root = doc.RootElement.TryGetProperty("stepData", out var sd) ? sd : doc.RootElement;
        var address = GetStr(root, "address");

        // validate ฝั่ง server เสมอ (อย่าเชื่อ FE)
        if (string.IsNullOrWhiteSpace(address))
            return Fail("Step.RequiredField", "ที่อยู่จำเป็นต้องกรอก");

        // อ่าน data ของ step อื่นผ่าน StepType (ไม่ใช่ index)
        var companyId = ReadCompanyId(ctx.StepData.GetValueOrDefault("JuristicBindingStep"));

        var output = JsonSerializer.Serialize(new { address, companyId, verifiedAt = DateTime.UtcNow.ToString("O") });
        return Ok(output); // engine จะ upsert เป็น FlowInstanceStepData
    }
}

2) INSERT StepCatalog

INSERT INTO "StepCatalog" ("StepTypeCode","ExecutionMode","SkipScope","DependenciesJson")
VALUES ('AddressVerificationStep', 0 /*Interactive*/, 2 /*PerUserPerCompany*/,
        '[{"StepTypeCode":"JuristicBindingStep","Mode":"Required"}]');

3) ผูกเข้า Flow (FlowStep)

INSERT INTO "FlowSteps" ("FlowDefinitionId","StepCatalogId","Order","StepTypeCode", ...)
VALUES (<flowDefId>, <stepCatalogId>, <order หลัง JuristicBinding>, 'AddressVerificationStep', ...);

4) register DI

// UserService02.Infrastructure/DependencyInjection.cs
services.AddScoped<IFlowStepHandler, AddressVerificationStepHandler>();

ฝั่ง Frontend (เฉพาะ Interactive step)

5) สร้าง component ตาม contract

component conform contract ของ StepHostDirective: input canEdit/context, output dataChange/validChange/action

@Component({ selector: 'app-address-verification', standalone: true, /* ... */ })
export class AddressVerificationComponent {
  canEdit = input<boolean>(true);
  dataChange = output<object>();
  validChange = output<boolean>();
  action = output<StepActionEmit>();

  onSubmit() {
    if (this.form.invalid) return;
    this.action.emit({ code: 'Next', payload: this.form.value });
  }
}

6) register ใน STEP_REGISTRY (1 บรรทัด)

// step-registry.ts — key ต้องตรงกับ StepType ฝั่ง BE
export const STEP_REGISTRY: Record<string, Type<unknown>> = {
  // ...
  AddressVerificationStep: AddressVerificationComponent,
};

7) (ถ้า component คุมปุ่มเอง) เพิ่มใน SELF_ACTION_STEPS

const SELF_ACTION_STEPS = new Set([... , 'AddressVerificationStep']);

Checklist

#ที่ทำอะไร
1BE HandlerIFlowStepHandler + StepType ตรง
2DB StepCatalogINSERT row (ExecutionMode/SkipScope/Deps)
3DB FlowStepผูกเข้า flow + Order
4BE DIAddScoped<IFlowStepHandler, X>()
5FE componentconform contract (Interactive เท่านั้น)
6FE registryเพิ่มใน STEP_REGISTRY
7FE (ถ้าจำเป็น)SELF_ACTION_STEPS / sidebar progress