Skip to content

Examples

Contribute to Electronic Approval

Connect the Workshop Note binding, business-form widget, and App-owned submission endpoint to the Approval composer.

Example type Recipe

Overview

Contribute to Electronic Approval

This recipe connects the Workshop Note from the previous recipes to the real Electronic Approval composer. The Descriptor recipe published the Submit Note choice; this recipe joins its Slot Widget, frontend controller, and App-owned submission endpoint with the same identity.

What appears when you finish

ContributionScreen or capability
WorkshopApprovalSlotWidgetsConnects Workshop to Core's business-form composer slot
Frontend registry and WidgetNote selection, name/status preview, and submit readiness
App submission endpointReauthorizes the actor and Note, then creates a Core Approval case
Published Legal Entity templateWorkshop business form under E-Approval → Create E-Approval request

Workshop Submit Note selected in an Approval template

The screenshot is the actual template form after selecting Submit Note from the Descriptor catalog. The App description, “Link an existing note to Electronic Approval,” appears below the selection. It reaches the end-user creation catalog only after an administrator creates and publishes this template.

How three contracts connect to Core's composer flow

This feature is not completed by extending one class. It connects three boundaries through the same workshop.note.submit identity.

BoundaryWhat Workshop implements or registersWhat Core processes
Static UI contractWorkshopApprovalSlotWidgets implements AppDescriptorContributionAppDescriptorCatalog and SlotWidgetCompositionResolver check App installation, slot API version, lifecycle, and permission
Browser contractapprovalComposerBusinessFormRegistry.register(...) and an ApprovalBusinessFormSlotPropsV2 controllerThe composer loads the Widget by component key and hosts the shared approval line, draft, preview, and submit lifecycle
Submission contractThe App endpoint receives Nexia\Approval\Contracts\ApprovalHost by dependency injectionCore's CoreApprovalHost implements ApprovalHost validates the binding and template and creates the Approval case and audit evidence
Published Legal Entity template
  → Core composer reads the binding formWidgetKey
  → SlotWidgetCompositionResolver selects an allowed Slot descriptor
  → frontend registry loads the Workshop Widget with the same key
  → Widget calls the App submission endpoint
  → App rechecks the Note, Policy, and revision
  → ApprovalHost::submitBound()
  → CoreApprovalHost creates the Core Approval case

The App endpoint neither imports nor extends CoreApprovalHost. Its constructor receives the SDK ApprovalHost interface, which the Core host binds to the CoreApprovalHost implementation. This boundary keeps Core unaware of the Workshop model and keeps Workshop away from Core Eloquent models.

1. Add the Slot Widget Descriptor

Code example
Shell
cp docs/developers/examples/approval-contribution/WorkshopApprovalSlotWidgets.php \
  packages/workshop/src/Descriptors/WorkshopApprovalSlotWidgets.php

Four identities must match exactly:

Approval binding formWidgetKey
        = Slot descriptor key
        = Slot descriptor component
        = frontend registry key
        = workshop.note.submit

The descriptor fills Core's approval.composer.business_form slot and gates visibility with workshop.note.publish.

2. Register the frontend Widget

Add the public App SDK registry call to packages/workshop/resources/js/index.ts:

Code example
TypeScript
import { approvalComposerBusinessFormRegistry } from '@nexia/sdk/host';

approvalComposerBusinessFormRegistry.register(
    'workshop.note.submit',
    () => import('./approval/WorkshopNoteApprovalWidget'),
);

WorkshopNoteApprovalWidget receives ApprovalBusinessFormSlotPropsV2, loads the Note, and registers the composer controller:

Code example
TSX
registerController({
    canSubmit: note !== null && note.status === 'active',
    saveDraft: async () => ({ appDraftRef: note.public_id }),
    buildPreview: () => ({
        title: note.name,
        sections: [{
            title: t('workshop.approval.note.document.summary'),
            fields: [
                { key: 'name', label: t('workshop.note.name.label'), value: note.name },
                { key: 'status', label: t('workshop.note.status.label'), value: note.status },
            ],
        }],
    }),
    submit: async (envelope) => {
        const response = await api.post(
            `/legal-entities/${legalEntityPublicId}/workshop/notes/${note.public_id}/submit-approval`,
            {
                template_key: envelope.templateKey,
                template_version: envelope.templateVersion,
                line: envelope.line,
                reference_user_ids: envelope.referenceUserIds,
                circulation_user_ids: envelope.circulationUserIds,
            },
        );
        return { caseId: response.data.approval_case.public_id };
    },
});

This shortened excerpt shows the controller boundary. A production Widget also handles loading, not-found, denial, idempotency, staged attachments, and error states. The browser calls an App endpoint, not a Core Approval endpoint.

3. Reauthorize and submit in the App

The App endpoint resolves the Note within the Legal Entity and rechecks the workshop.note.publish Policy. It locks and verifies current state and revision before using the App SDK ApprovalHost:

Code example
PHP
$case = $this->approvals->submitBound(
    new BoundApprovalSubmission(
        legalEntity: $legalEntity,
        drafter: $actor,
        templateKey: $input['template_key'],
        templateVersion: (int) $input['template_version'],
        bindingKey: 'workshop.note.submit',
        bindingVersion: '1.0',
        resourceRef: new ResourceRef(
            appKey: 'workshop',
            resourceKey: 'workshop.note',
            resourceId: (string) $note->public_id,
            display: $note->name,
            href: '/apps/workshop/notes/'.$note->public_id,
        ),
        resourceVersion: (string) $note->updated_at->getTimestamp(),
        documentValues: [
            'name' => $note->name,
            'status' => $note->status->value,
        ],
        lineDefinition: $line,
        title: $note->name,
    ),
);

The App owns the Note and its Policy; Core owns approval lines, cases, and audit evidence. Neither side imports the other's model.

4. Publish the template and observe it

  1. Under E-Approval → Template management → New template, choose Business form.
  2. Select Domain form → Workshop → Submit Note.
  3. Set its name, Legal Entity scope, and approval rules, then create and publish it.
  4. Open E-Approval → Create E-Approval request and choose the published Workshop form.
  5. Link a Note and confirm its name/status beside the shared approval line.
  6. Submit and confirm navigation to the created Approval case.

A binding by itself is intentionally absent from the end-user creation catalog. That catalog lists published Legal Entity templates, not every active descriptor.

5. Inspect discovery

Code example
Shell
sh docs/developers/examples/approval-contribution/verify.sh

Static contribution verification is complete when the script prints Workshop Approval contribution discovered and validated.

Common errors

SymptomCauseFixConfirm
Submit Note is absent from the domain-form catalogWorkshop is not installed, submit authority is absent, or binding lifecycle or locale resolution failsCheck installation in the same tenant, workshop.note.publish, an Active binding, and its locale keysWorkshop → Submit Note is selectable as a domain form
The domain form exists but the creation catalog has no entryThe binding exists, but no Legal Entity template has been created and publishedCreate and publish the Workshop domain-form template in that Legal EntityThe published template appears under Create E-Approval request
The composer cannot resolve the WidgetThe binding, Slot descriptor, and frontend registry use different component identitiesSet all four identities to workshop.note.submit and refresh the frontendThe composer loads the existing-Note selection Widget
Submission is deniedThe App Route guard, Note Policy, revision, or Access Grant scope check failsRecheck workshop.note.publish, Note state and revision for the current actor and Legal EntitySuccessful submission navigates to the created Approval case

Verify behavior across settings

This Workshop example verifies a minimal contribution. It does not automatically add configurable approval requirements or default procedure installation. Read Electronic Approval for the developer's enforcement obligations and the administrator's form, line, and requirement setup. Then follow the real purchase flow in Business Process to compare automatic inputs, human review, required versus optional approval, line preparation, outcome branches, and supplied-template updates in one journey.

Included files

Approval Slot Widget descriptor

docs/developers/examples/approval-contribution/WorkshopApprovalSlotWidgets.php
Code example
PHP
<?php

declare(strict_types=1);

namespace Amuzcorp\Nexia\Workshop\Descriptors;

use Nexia\AppDescriptors\AppDescriptorSet;
use Nexia\AppDescriptors\Contracts\AppDescriptorContribution;
use Nexia\AppDescriptors\DescriptorStatus;
use Nexia\AppDescriptors\SlotWidgetDescriptor;

final class WorkshopApprovalSlotWidgets implements AppDescriptorContribution
{
    public static function appDescriptors(): AppDescriptorSet
    {
        return AppDescriptorSet::of(
            new SlotWidgetDescriptor(
                key: 'workshop.note.submit',
                version: '1.0',
                slot: 'approval.composer.business_form',
                component: 'workshop.note.submit',
                slotApiVersion: 2,
                status: DescriptorStatus::Active,
                permission: 'workshop.note.publish',
                labelKey: 'workshop.approval.note.submit.label',
            ),
        );
    }
}

Refresh and verify the Workshop contribution

docs/developers/examples/approval-contribution/verify.sh
Code example
Shell
#!/usr/bin/env sh
set -eu

for required in \
  packages/workshop/src/Descriptors/WorkshopApprovalDescriptors.php \
  packages/workshop/src/Descriptors/WorkshopApprovalSlotWidgets.php \
  packages/workshop/resources/js/approval/WorkshopNoteApprovalWidget.tsx \
  packages/workshop/resources/js/index.ts
do
  test -f "$required" || {
    printf 'Missing Workshop Approval source: %s\n' "$required" >&2
    exit 1
  }
done

task artisan -- nexia-runtime:refresh-runtime-caches --if-route-cached --no-ansi
task artisan -- nexia-apps:validate-app-descriptors workshop --no-ansi
task artisan -- nexia-apps:doctor-package-app workshop --no-ansi

printf '%s\n' 'Workshop Approval contribution discovered and validated. Confirm Submit Note in Electronic Approval.'