Taryana
Automated SPK EngineERP & Sales Operations
TESTER READY • LIVE ENGINE

Sales Order SPK & Automated Provisioning Lifecycle

Customer Onboarding, ODP Port Allocation & RADIUS Provisioning

Otomasi penerbitan Surat Perintah Kerja (SPK) pasang baru pelanggan, validasi KTP/NIK, reservasi port ODP selama 24 jam, dan pembuatan akun PPPoE/RADIUS otomatis di NestJS.

Diagram Alur Data & Arsitektur Sistem

Visual Interaktif

Alur Penerbitan SPK Pasang Baru & Pre-Provisioning RADIUS

Penguncian port ODP atomik dengan auto-provisioning FreeRADIUS

client
Field Sales

Sales Portal

Input Data Prospek

POST /orders/spk
service
ERP Core

NestJS SPK Engine

Order Verification

Pessimistic lock port
db
SERIALIZABLE

PostgreSQL DB

Atomic SPK & Port Tx

SETNX lock:odp:port
cache
Distributed Lock

Redis Port Lock

TTL 24h Reservation

Provision PPPoE account
device
Network Core

FreeRADIUS Server

Pre-provision PPPoE

Problem & Latar Belakang Kebutuhan

Pada operasional sales ISP konvensional, proses pengajuan pasang baru pelanggan dilakukan manual dengan form cetak atau spreadsheet. Hal ini sering menimbulkan insiden double-booking port splitter ODP yang sama saat teknisi tiba di lokasi, serta keterlambatan aktivasi internet karena tim admin NOC harus membuat profil PPPoE di MikroTik/FreeRADIUS secara manual satu per satu.

Implementasi ServiceOrderProvisioningService di NestJS

Service NestJS memvalidasi data prospek dan titik koordinat geospasial, kemudian menjalankan transaksi atomik: mengunci nomor port pada tabel odp_nodes, menerbitkan record SPK bernomor urut terisolasi (contoh: SPK-2026-JKT-091), dan memicu background worker untuk mengonfigurasi profil bandwidth PPPoE di server FreeRADIUS / MikroTik via UDP CoA / API.

Aspek Skema Relasional PostgreSQL & State Machine SPK

Tabel PostgreSQL service_orders menyimpan referensi relasional ke calon pelanggan, titik koordinat GPS, paket langganan, dan foreign key odp_node_id. State machine transisi pesanan: DRAFT -> SPK_ISSUED -> ODP_RESERVED -> FIELD_INSTALLED -> ACTIVATED.

Keamanan, Idempotensi & Kepatuhan SLA

Reservasi port ODP dikunci di Redis dengan TTL 86.400 detik (24 jam) menggunakan key lock:odp:{id}:port:{index}. Jika instalasi fisik batal atau melewati batas waktu 24 jam, port otomatis rilis kembali ke pool ketersediaan tanpa intervensi manual.

NestJS ServiceOrderProvisioningService & ODP LockNestJS (TypeScript)
// Service Order SPK & Automated Provisioning Service di NestJS
@Injectable()
export class ServiceOrderProvisioningService {
  constructor(
    @InjectDataSource() private readonly dataSource: DataSource,
    private readonly redisService: RedisService,
    private readonly radiusClient: RadiusClientService,
  ) {}

  async createServiceOrder(dto: CreateOrderSpkDto): Promise<ServiceOrderResponse> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction('SERIALIZABLE');

    try {
      // 1. Verifikasi ketersediaan port ODP dengan pessimistic write lock
      const odp = await queryRunner.manager.createQueryBuilder(OdpNode, 'odp')
        .setLock('pessimistic_write')
        .where('odp.id = :id', { id: dto.odpId })
        .getOne();

      if (!odp || odp.usedPorts >= odp.totalPorts) {
        throw new ConflictException('Port splitter ODP telah penuh.');
      }

      // 2. Lock port ODP di Redis (TTL 24 Jam)
      const lockKey = `lock:odp:${odp.id}:port:${odp.usedPorts + 1}`;
      const locked = await this.redisService.set(lockKey, dto.customerNik, 'EX', 86400, 'NX');
      if (!locked) {
        throw new ConflictException('Port sedang dalam reservasi aktif SPK lain.');
      }

      // 3. Terbitkan record SPK bernomor urut resmi
      const spkNumber = `SPK-${new Date().getFullYear()}-${dto.branchCode}-${Math.floor(1000 + Math.random() * 9000)}`;
      const order = queryRunner.manager.create(ServiceOrder, {
        spkNumber,
        customerName: dto.customerName,
        customerNik: dto.customerNik,
        customerPhone: dto.customerPhone,
        packageCode: dto.packageCode,
        odpId: odp.id,
        portIndex: odp.usedPorts + 1,
        status: 'SPK_ISSUED',
        createdAt: new Date(),
      });
      await queryRunner.manager.save(order);

      // Increment port used count
      await queryRunner.manager.update(OdpNode, odp.id, {
        usedPorts: odp.usedPorts + 1,
      });

      await queryRunner.commitTransaction();

      // 4. Pre-provision akun PPPoE di FreeRADIUS
      await this.radiusClient.createSubscriberAccount({
        username: `user_${dto.customerPhone.slice(-6)}`,
        profileSpeed: dto.packageCode,
        nasIp: odp.nasIp,
      });

      return { spkNumber, status: 'SPK_ISSUED', reservedPort: odp.usedPorts + 1 };
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }
}

Live Simulator & Execution Sandbox

Interactive State Tester
Status Integrasi FreeRADIUS & ODP:Port 4/8 Terkunci (TTL 24 Jam) • Profil RADIUS SiapPenerbitan SPK otomatis mengunci port splitter ODP dan membuat akun PPPoE sementara.
Execution Telemetry Console
HTTP 200 OK
POST/api/v1/network/coverage/check
> Guard Evaluation Trace:

PostGIS GiST R-Tree ST_DWithin query executed in 3.8ms. Matched ODP-FATMAWATI-42 (84.2m).

> Database & State Result:

Estimated optical attenuation: -17.4 dBm. Port 4 of 8 available. Redis lock acquired with 24h TTL.

Audit Trail PostgreSQL & Redis (2 events)Auto-Appended
[10:14:02]SETTLEMENT(Double-Entry Worker)•WEBHOOK_INVOICE_SETTLED

HMAC-SHA256 verified. Redis Redlock acquired. Invoice INV-2026-0982 settled with zero ledger variance.

SUCCESS
[10:22:15]POSTGIS(Coverage Engine (NestJS))•ODP_PORT_RESERVED

ST_DWithin R-Tree matched ODP in 4.1ms (dist: 84.2m). Optical loss -17.4 dBm. Splitter port locked with 24h Redis TTL.

SUCCESS