This repo had no version control prior to this commit. The import is a
straight snapshot of the working tree at 2026-05-03; the deployed
binary on fihelvop01 was being rebuilt from this source via `make
build` + scp into place, with no upstream review path.
The snapshot already includes one in-flight fix made on 2026-05-03 to
internal/service/persona.go:GetSelfModel — the handler queried
`source` and `strength` columns plus an `is_active = true` filter on
persona.persona_commitments, none of which exist on that table (its
shape is session-bound commitments with `status`, `commitment_meta`,
etc.). The query returned a 500 every time SynapseHub bootstrapped a
persona's self-model, dropping the IdentityConstraints / Commitments /
ConscienceStandards layer from the assembled prompt. The patched
query reads existing columns only (commitment_text, commitment_type),
filters on `status='active'`, and synthesises Source="learned" /
Strength=1.0 to keep the SelfModel response shape stable for callers.
Verified live: `GET /api/v1/personas/70f7cfd9-.../self-model` now
returns 200 with `{identityConstraints:[],commitments:[],
conscienceStandards:[]}` instead of 500.
Future changes go through PRs against this repo — no more bin-only
deploys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1092 lines
35 KiB
Go
1092 lines
35 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/gosec/gsc-ops-api/internal/client"
|
|
"github.com/gosec/gsc-ops-api/pkg/types"
|
|
)
|
|
|
|
// PBXService handles PBX trunk, DID, extension, and route operations
|
|
type PBXService struct {
|
|
pool *pgxpool.Pool // gsc_admin database
|
|
asterisk *client.AsteriskClient
|
|
kamailio *client.KamailioClient
|
|
logger zerolog.Logger
|
|
}
|
|
|
|
// NewPBXService creates a new PBX service
|
|
func NewPBXService(pool *pgxpool.Pool, asterisk *client.AsteriskClient, kamailio *client.KamailioClient, logger zerolog.Logger) *PBXService {
|
|
return &PBXService{
|
|
pool: pool,
|
|
asterisk: asterisk,
|
|
kamailio: kamailio,
|
|
logger: logger.With().Str("service", "pbx").Logger(),
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Trunks
|
|
// ============================================================================
|
|
|
|
// ListTrunks lists SIP trunks with optional filters
|
|
func (s *PBXService) ListTrunks(ctx context.Context, params types.ListParams) ([]types.PBXTrunk, int64, error) {
|
|
params = types.DefaultListParams(params)
|
|
|
|
countQuery := `SELECT COUNT(*) FROM pbx_trunks WHERE 1=1`
|
|
listQuery := `SELECT t.id, t.tenant_id, t.provider_id, t.name, COALESCE(t.description,''),
|
|
t.trunk_type, t.status,
|
|
t.host, t.port, t.transport, COALESCE(t.username,''), COALESCE(t.auth_realm,''),
|
|
COALESCE(t.from_domain,''), COALESCE(t.from_user,''),
|
|
t.register, t.codecs, t.dtmf_mode, t.nat_mode,
|
|
t.max_channels, t.current_channels,
|
|
COALESCE(t.outbound_caller_id_name,''), COALESCE(t.outbound_caller_id_number,''),
|
|
t.priority,
|
|
t.created_at, t.updated_at,
|
|
(SELECT COUNT(*) FROM pbx_trunk_dids d WHERE d.trunk_id = t.id) AS did_count
|
|
FROM pbx_trunks t WHERE 1=1`
|
|
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if params.Status != "" {
|
|
countQuery += fmt.Sprintf(" AND status = $%d", argIdx)
|
|
listQuery += fmt.Sprintf(" AND t.status = $%d", argIdx)
|
|
args = append(args, params.Status)
|
|
argIdx++
|
|
}
|
|
if params.Search != "" {
|
|
countQuery += fmt.Sprintf(" AND (name ILIKE $%d OR host ILIKE $%d OR username ILIKE $%d)", argIdx, argIdx, argIdx)
|
|
listQuery += fmt.Sprintf(" AND (t.name ILIKE $%d OR t.host ILIKE $%d OR t.username ILIKE $%d)", argIdx, argIdx, argIdx)
|
|
args = append(args, "%"+params.Search+"%")
|
|
argIdx++
|
|
}
|
|
|
|
var total int64
|
|
if err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil {
|
|
return nil, 0, fmt.Errorf("count query failed: %w", err)
|
|
}
|
|
|
|
listQuery += fmt.Sprintf(" ORDER BY t.created_at DESC LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
|
|
args = append(args, params.Limit, params.Offset)
|
|
|
|
rows, err := s.pool.Query(ctx, listQuery, args...)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("list query failed: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
trunks := make([]types.PBXTrunk, 0)
|
|
for rows.Next() {
|
|
var t types.PBXTrunk
|
|
if err := rows.Scan(&t.ID, &t.TenantID, &t.ProviderID, &t.Name, &t.Description,
|
|
&t.TrunkType, &t.Status,
|
|
&t.Host, &t.Port, &t.Transport, &t.Username, &t.AuthRealm,
|
|
&t.FromDomain, &t.FromUser,
|
|
&t.Register, &t.Codecs, &t.DtmfMode, &t.NatMode,
|
|
&t.MaxChannels, &t.CurrentChannels,
|
|
&t.OutboundCallerIDName, &t.OutboundCallerIDNum,
|
|
&t.Priority,
|
|
&t.CreatedAt, &t.UpdatedAt, &t.DIDCount); err != nil {
|
|
return nil, 0, fmt.Errorf("scan failed: %w", err)
|
|
}
|
|
trunks = append(trunks, t)
|
|
}
|
|
|
|
return trunks, total, nil
|
|
}
|
|
|
|
// GetTrunk gets a trunk by ID
|
|
func (s *PBXService) GetTrunk(ctx context.Context, id uuid.UUID) (*types.PBXTrunk, error) {
|
|
var t types.PBXTrunk
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT t.id, t.tenant_id, t.provider_id, t.name, COALESCE(t.description,''),
|
|
t.trunk_type, t.status,
|
|
t.host, t.port, t.transport, COALESCE(t.username,''), COALESCE(t.auth_realm,''),
|
|
COALESCE(t.from_domain,''), COALESCE(t.from_user,''),
|
|
t.register, t.codecs, t.dtmf_mode, t.nat_mode,
|
|
t.max_channels, t.current_channels,
|
|
COALESCE(t.outbound_caller_id_name,''), COALESCE(t.outbound_caller_id_number,''),
|
|
t.priority,
|
|
t.created_at, t.updated_at,
|
|
(SELECT COUNT(*) FROM pbx_trunk_dids d WHERE d.trunk_id = t.id) AS did_count
|
|
FROM pbx_trunks t WHERE t.id = $1`, id).
|
|
Scan(&t.ID, &t.TenantID, &t.ProviderID, &t.Name, &t.Description,
|
|
&t.TrunkType, &t.Status,
|
|
&t.Host, &t.Port, &t.Transport, &t.Username, &t.AuthRealm,
|
|
&t.FromDomain, &t.FromUser,
|
|
&t.Register, &t.Codecs, &t.DtmfMode, &t.NatMode,
|
|
&t.MaxChannels, &t.CurrentChannels,
|
|
&t.OutboundCallerIDName, &t.OutboundCallerIDNum,
|
|
&t.Priority,
|
|
&t.CreatedAt, &t.UpdatedAt, &t.DIDCount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
// CreateTrunk creates a new SIP trunk
|
|
func (s *PBXService) CreateTrunk(ctx context.Context, req *types.PBXTrunkCreate) (*types.PBXTrunk, error) {
|
|
id := uuid.New()
|
|
now := time.Now().UTC()
|
|
|
|
port := req.Port
|
|
if port == 0 {
|
|
port = 5060
|
|
}
|
|
transport := req.Transport
|
|
if transport == "" {
|
|
transport = "udp"
|
|
}
|
|
codecs := req.Codecs
|
|
if len(codecs) == 0 {
|
|
codecs = []string{"ulaw", "alaw"}
|
|
}
|
|
dtmfMode := req.DtmfMode
|
|
if dtmfMode == "" {
|
|
dtmfMode = "rfc4733"
|
|
}
|
|
|
|
natMode := req.NatMode
|
|
if natMode == "" {
|
|
natMode = "yes"
|
|
}
|
|
|
|
_, err := s.pool.Exec(ctx,
|
|
`INSERT INTO pbx_trunks (id, tenant_id, provider_id, name, description, trunk_type, status,
|
|
host, port, transport, username, auth_realm, from_domain, from_user,
|
|
register, codecs, dtmf_mode, nat_mode, max_channels,
|
|
outbound_caller_id_name, outbound_caller_id_number,
|
|
created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, 'pjsip', 'inactive',
|
|
$6, $7, $8, $9, $10, $11, $12,
|
|
$13, $14, $15, $16, $17,
|
|
$18, $19,
|
|
$20, $20)`,
|
|
id, req.TenantID, req.ProviderID, req.Name, nilIfEmpty(req.Description),
|
|
req.Host, port, transport, nilIfEmpty(req.Username), nilIfEmpty(req.AuthRealm),
|
|
nilIfEmpty(req.FromDomain), nilIfEmpty(req.FromUser),
|
|
req.Register, codecs, dtmfMode, natMode, req.MaxChannels,
|
|
nilIfEmpty(req.OutboundCallerIDName), nilIfEmpty(req.OutboundCallerIDNum),
|
|
now)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert failed: %w", err)
|
|
}
|
|
|
|
// If password provided, store in sip_passwords via asterisk DB
|
|
// (handled by caller or separate sync endpoint)
|
|
|
|
return s.GetTrunk(ctx, id)
|
|
}
|
|
|
|
// UpdateTrunk updates a trunk
|
|
func (s *PBXService) UpdateTrunk(ctx context.Context, id uuid.UUID, req *types.PBXTrunkUpdate) (*types.PBXTrunk, error) {
|
|
setClauses := []string{}
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if req.Name != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("name = $%d", argIdx))
|
|
args = append(args, *req.Name)
|
|
argIdx++
|
|
}
|
|
if req.Description != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("description = $%d", argIdx))
|
|
args = append(args, *req.Description)
|
|
argIdx++
|
|
}
|
|
if req.Host != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("host = $%d", argIdx))
|
|
args = append(args, *req.Host)
|
|
argIdx++
|
|
}
|
|
if req.Port != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("port = $%d", argIdx))
|
|
args = append(args, *req.Port)
|
|
argIdx++
|
|
}
|
|
if req.Transport != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("transport = $%d", argIdx))
|
|
args = append(args, *req.Transport)
|
|
argIdx++
|
|
}
|
|
if req.Username != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("username = $%d", argIdx))
|
|
args = append(args, *req.Username)
|
|
argIdx++
|
|
}
|
|
if req.AuthRealm != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("auth_realm = $%d", argIdx))
|
|
args = append(args, *req.AuthRealm)
|
|
argIdx++
|
|
}
|
|
if req.FromDomain != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("from_domain = $%d", argIdx))
|
|
args = append(args, *req.FromDomain)
|
|
argIdx++
|
|
}
|
|
if req.FromUser != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("from_user = $%d", argIdx))
|
|
args = append(args, *req.FromUser)
|
|
argIdx++
|
|
}
|
|
if req.Register != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("register = $%d", argIdx))
|
|
args = append(args, *req.Register)
|
|
argIdx++
|
|
}
|
|
if req.Codecs != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("codecs = $%d", argIdx))
|
|
args = append(args, req.Codecs)
|
|
argIdx++
|
|
}
|
|
if req.DtmfMode != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("dtmf_mode = $%d", argIdx))
|
|
args = append(args, *req.DtmfMode)
|
|
argIdx++
|
|
}
|
|
if req.NatMode != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("nat_mode = $%d", argIdx))
|
|
args = append(args, *req.NatMode)
|
|
argIdx++
|
|
}
|
|
if req.MaxChannels != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("max_channels = $%d", argIdx))
|
|
args = append(args, *req.MaxChannels)
|
|
argIdx++
|
|
}
|
|
if req.OutboundCallerIDName != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("outbound_caller_id_name = $%d", argIdx))
|
|
args = append(args, *req.OutboundCallerIDName)
|
|
argIdx++
|
|
}
|
|
if req.OutboundCallerIDNum != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("outbound_caller_id_number = $%d", argIdx))
|
|
args = append(args, *req.OutboundCallerIDNum)
|
|
argIdx++
|
|
}
|
|
if req.Status != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("status = $%d", argIdx))
|
|
args = append(args, *req.Status)
|
|
argIdx++
|
|
}
|
|
|
|
if len(setClauses) == 0 {
|
|
return s.GetTrunk(ctx, id)
|
|
}
|
|
|
|
setClauses = append(setClauses, fmt.Sprintf("updated_at = $%d", argIdx))
|
|
args = append(args, time.Now().UTC())
|
|
argIdx++
|
|
|
|
args = append(args, id)
|
|
query := fmt.Sprintf("UPDATE pbx_trunks SET %s WHERE id = $%d",
|
|
join(setClauses, ", "), argIdx)
|
|
|
|
_, err := s.pool.Exec(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("update failed: %w", err)
|
|
}
|
|
|
|
return s.GetTrunk(ctx, id)
|
|
}
|
|
|
|
// DeleteTrunk soft-deletes a trunk by setting status to inactive
|
|
func (s *PBXService) DeleteTrunk(ctx context.Context, id uuid.UUID) error {
|
|
_, err := s.pool.Exec(ctx,
|
|
`UPDATE pbx_trunks SET status = 'inactive', updated_at = $1 WHERE id = $2`,
|
|
time.Now().UTC(), id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Reload Asterisk and Kamailio to remove the trunk
|
|
if s.asterisk != nil {
|
|
go s.asterisk.ReloadPJSIP()
|
|
}
|
|
if s.kamailio != nil {
|
|
go s.kamailio.ReloadDispatcher()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ActivateTrunk sets trunk status to active and reloads Asterisk + Kamailio
|
|
func (s *PBXService) ActivateTrunk(ctx context.Context, id uuid.UUID) (*types.PBXTrunk, error) {
|
|
_, err := s.pool.Exec(ctx,
|
|
`UPDATE pbx_trunks SET status = 'active', updated_at = $1 WHERE id = $2`,
|
|
time.Now().UTC(), id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("activate failed: %w", err)
|
|
}
|
|
|
|
// Reload Asterisk PJSIP to pick up new trunk
|
|
if s.asterisk != nil {
|
|
go s.asterisk.ReloadPJSIP()
|
|
}
|
|
|
|
// Reload Kamailio dispatcher in case routing changed
|
|
if s.kamailio != nil {
|
|
go s.kamailio.ReloadDispatcher()
|
|
}
|
|
|
|
return s.GetTrunk(ctx, id)
|
|
}
|
|
|
|
// DeactivateTrunk sets trunk status to inactive and reloads Asterisk + Kamailio
|
|
func (s *PBXService) DeactivateTrunk(ctx context.Context, id uuid.UUID) (*types.PBXTrunk, error) {
|
|
_, err := s.pool.Exec(ctx,
|
|
`UPDATE pbx_trunks SET status = 'inactive', updated_at = $1 WHERE id = $2`,
|
|
time.Now().UTC(), id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("deactivate failed: %w", err)
|
|
}
|
|
|
|
// Reload Asterisk
|
|
if s.asterisk != nil {
|
|
go s.asterisk.ReloadPJSIP()
|
|
}
|
|
|
|
// Reload Kamailio dispatcher
|
|
if s.kamailio != nil {
|
|
go s.kamailio.ReloadDispatcher()
|
|
}
|
|
|
|
return s.GetTrunk(ctx, id)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Trunk DIDs
|
|
// ============================================================================
|
|
|
|
// ListTrunkDIDs lists DIDs for a trunk
|
|
func (s *PBXService) ListTrunkDIDs(ctx context.Context, trunkID uuid.UUID) ([]types.PBXTrunkDID, error) {
|
|
rows, err := s.pool.Query(ctx,
|
|
`SELECT id, trunk_id, tenant_id, did_number, description,
|
|
destination_type, destination_id, is_active, created_at
|
|
FROM pbx_trunk_dids WHERE trunk_id = $1 ORDER BY did_number`, trunkID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query failed: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
dids := make([]types.PBXTrunkDID, 0)
|
|
for rows.Next() {
|
|
var d types.PBXTrunkDID
|
|
if err := rows.Scan(&d.ID, &d.TrunkID, &d.TenantID, &d.DIDNumber, &d.Description,
|
|
&d.DestinationType, &d.DestinationID, &d.IsActive, &d.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("scan failed: %w", err)
|
|
}
|
|
dids = append(dids, d)
|
|
}
|
|
return dids, nil
|
|
}
|
|
|
|
// CreateTrunkDID adds a DID to a trunk
|
|
func (s *PBXService) CreateTrunkDID(ctx context.Context, trunkID uuid.UUID, req *types.PBXTrunkDIDCreate) (*types.PBXTrunkDID, error) {
|
|
id := uuid.New()
|
|
now := time.Now().UTC()
|
|
|
|
_, err := s.pool.Exec(ctx,
|
|
`INSERT INTO pbx_trunk_dids (id, trunk_id, tenant_id, did_number, description,
|
|
destination_type, destination_id, is_active, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, true, $8, $8)`,
|
|
id, trunkID, req.TenantID, req.DIDNumber, nilIfEmpty(req.Description),
|
|
nilIfEmpty(req.DestinationType), req.DestinationID, now)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert failed: %w", err)
|
|
}
|
|
|
|
var d types.PBXTrunkDID
|
|
err = s.pool.QueryRow(ctx,
|
|
`SELECT id, trunk_id, tenant_id, did_number, description,
|
|
destination_type, destination_id, is_active, created_at
|
|
FROM pbx_trunk_dids WHERE id = $1`, id).
|
|
Scan(&d.ID, &d.TrunkID, &d.TenantID, &d.DIDNumber, &d.Description,
|
|
&d.DestinationType, &d.DestinationID, &d.IsActive, &d.CreatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &d, nil
|
|
}
|
|
|
|
// DeleteTrunkDID removes a DID from a trunk
|
|
func (s *PBXService) DeleteTrunkDID(ctx context.Context, trunkID, didID uuid.UUID) error {
|
|
_, err := s.pool.Exec(ctx,
|
|
`DELETE FROM pbx_trunk_dids WHERE id = $1 AND trunk_id = $2`, didID, trunkID)
|
|
return err
|
|
}
|
|
|
|
// ============================================================================
|
|
// Extensions
|
|
// ============================================================================
|
|
|
|
// ListExtensions lists extensions with optional filters
|
|
func (s *PBXService) ListExtensions(ctx context.Context, params types.ListParams) ([]types.PBXExtension, int64, error) {
|
|
params = types.DefaultListParams(params)
|
|
|
|
countQuery := `SELECT COUNT(*) FROM pbx_extensions WHERE 1=1`
|
|
listQuery := `SELECT id, tenant_id, user_id, extension, name, extension_type,
|
|
caller_id_name, caller_id_number, outbound_caller_id_number,
|
|
sip_username, transport, codecs, voicemail_enabled, is_active,
|
|
created_at, updated_at
|
|
FROM pbx_extensions WHERE 1=1`
|
|
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if params.Status != "" {
|
|
if params.Status == "active" {
|
|
countQuery += " AND is_active = true"
|
|
listQuery += " AND is_active = true"
|
|
} else if params.Status == "inactive" {
|
|
countQuery += " AND is_active = false"
|
|
listQuery += " AND is_active = false"
|
|
}
|
|
}
|
|
if params.Search != "" {
|
|
countQuery += fmt.Sprintf(" AND (extension ILIKE $%d OR name ILIKE $%d OR sip_username ILIKE $%d)", argIdx, argIdx, argIdx)
|
|
listQuery += fmt.Sprintf(" AND (extension ILIKE $%d OR name ILIKE $%d OR sip_username ILIKE $%d)", argIdx, argIdx, argIdx)
|
|
args = append(args, "%"+params.Search+"%")
|
|
argIdx++
|
|
}
|
|
|
|
var total int64
|
|
if err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil {
|
|
return nil, 0, fmt.Errorf("count query failed: %w", err)
|
|
}
|
|
|
|
listQuery += fmt.Sprintf(" ORDER BY extension ASC LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
|
|
args = append(args, params.Limit, params.Offset)
|
|
|
|
rows, err := s.pool.Query(ctx, listQuery, args...)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("list query failed: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
exts := make([]types.PBXExtension, 0)
|
|
for rows.Next() {
|
|
var e types.PBXExtension
|
|
if err := rows.Scan(&e.ID, &e.TenantID, &e.UserID, &e.Extension, &e.Name, &e.ExtensionType,
|
|
&e.CallerIDName, &e.CallerIDNumber, &e.OutboundCallerIDNumber,
|
|
&e.SIPUsername, &e.Transport, &e.Codecs, &e.VoicemailEnabled, &e.IsActive,
|
|
&e.CreatedAt, &e.UpdatedAt); err != nil {
|
|
return nil, 0, fmt.Errorf("scan failed: %w", err)
|
|
}
|
|
exts = append(exts, e)
|
|
}
|
|
|
|
return exts, total, nil
|
|
}
|
|
|
|
// GetExtension gets an extension by ID
|
|
func (s *PBXService) GetExtension(ctx context.Context, id uuid.UUID) (*types.PBXExtension, error) {
|
|
var e types.PBXExtension
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT id, tenant_id, user_id, extension, name, extension_type,
|
|
caller_id_name, caller_id_number, outbound_caller_id_number,
|
|
sip_username, transport, codecs, voicemail_enabled, is_active,
|
|
created_at, updated_at
|
|
FROM pbx_extensions WHERE id = $1`, id).
|
|
Scan(&e.ID, &e.TenantID, &e.UserID, &e.Extension, &e.Name, &e.ExtensionType,
|
|
&e.CallerIDName, &e.CallerIDNumber, &e.OutboundCallerIDNumber,
|
|
&e.SIPUsername, &e.Transport, &e.Codecs, &e.VoicemailEnabled, &e.IsActive,
|
|
&e.CreatedAt, &e.UpdatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &e, nil
|
|
}
|
|
|
|
// CreateExtension creates a new extension
|
|
func (s *PBXService) CreateExtension(ctx context.Context, req *types.PBXExtensionCreate) (*types.PBXExtension, error) {
|
|
id := uuid.New()
|
|
now := time.Now().UTC()
|
|
|
|
extType := req.ExtensionType
|
|
if extType == "" {
|
|
extType = "user"
|
|
}
|
|
transport := req.Transport
|
|
if transport == "" {
|
|
transport = "udp"
|
|
}
|
|
codecs := req.Codecs
|
|
if len(codecs) == 0 {
|
|
codecs = []string{"opus", "ulaw", "alaw"}
|
|
}
|
|
sipUsername := req.SIPUsername
|
|
if sipUsername == "" {
|
|
sipUsername = req.Extension
|
|
}
|
|
|
|
_, err := s.pool.Exec(ctx,
|
|
`INSERT INTO pbx_extensions (id, tenant_id, user_id, extension, name, extension_type,
|
|
caller_id_name, caller_id_number, outbound_caller_id_number,
|
|
sip_username, transport, codecs, voicemail_enabled, is_active,
|
|
created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, true, $14, $14)`,
|
|
id, req.TenantID, req.UserID, req.Extension, req.Name, extType,
|
|
nilIfEmpty(req.CallerIDName), nilIfEmpty(req.CallerIDNumber),
|
|
nilIfEmpty(req.OutboundCallerIDNumber),
|
|
sipUsername, transport, codecs, req.VoicemailEnabled, now)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert failed: %w", err)
|
|
}
|
|
|
|
return s.GetExtension(ctx, id)
|
|
}
|
|
|
|
// UpdateExtension updates an extension
|
|
func (s *PBXService) UpdateExtension(ctx context.Context, id uuid.UUID, req *types.PBXExtensionUpdate) (*types.PBXExtension, error) {
|
|
setClauses := []string{}
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if req.Name != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("name = $%d", argIdx))
|
|
args = append(args, *req.Name)
|
|
argIdx++
|
|
}
|
|
if req.CallerIDName != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("caller_id_name = $%d", argIdx))
|
|
args = append(args, *req.CallerIDName)
|
|
argIdx++
|
|
}
|
|
if req.CallerIDNumber != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("caller_id_number = $%d", argIdx))
|
|
args = append(args, *req.CallerIDNumber)
|
|
argIdx++
|
|
}
|
|
if req.OutboundCallerIDNumber != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("outbound_caller_id_number = $%d", argIdx))
|
|
args = append(args, *req.OutboundCallerIDNumber)
|
|
argIdx++
|
|
}
|
|
if req.Codecs != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("codecs = $%d", argIdx))
|
|
args = append(args, req.Codecs)
|
|
argIdx++
|
|
}
|
|
if req.VoicemailEnabled != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("voicemail_enabled = $%d", argIdx))
|
|
args = append(args, *req.VoicemailEnabled)
|
|
argIdx++
|
|
}
|
|
if req.IsActive != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("is_active = $%d", argIdx))
|
|
args = append(args, *req.IsActive)
|
|
argIdx++
|
|
}
|
|
|
|
if len(setClauses) == 0 {
|
|
return s.GetExtension(ctx, id)
|
|
}
|
|
|
|
setClauses = append(setClauses, fmt.Sprintf("updated_at = $%d", argIdx))
|
|
args = append(args, time.Now().UTC())
|
|
argIdx++
|
|
|
|
args = append(args, id)
|
|
query := fmt.Sprintf("UPDATE pbx_extensions SET %s WHERE id = $%d",
|
|
join(setClauses, ", "), argIdx)
|
|
|
|
_, err := s.pool.Exec(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("update failed: %w", err)
|
|
}
|
|
|
|
return s.GetExtension(ctx, id)
|
|
}
|
|
|
|
// DeleteExtension soft-deletes an extension
|
|
func (s *PBXService) DeleteExtension(ctx context.Context, id uuid.UUID) error {
|
|
_, err := s.pool.Exec(ctx,
|
|
`UPDATE pbx_extensions SET is_active = false, updated_at = $1 WHERE id = $2`,
|
|
time.Now().UTC(), id)
|
|
return err
|
|
}
|
|
|
|
// ============================================================================
|
|
// Inbound Routes
|
|
// ============================================================================
|
|
|
|
// ListInboundRoutes lists inbound routes
|
|
func (s *PBXService) ListInboundRoutes(ctx context.Context, params types.ListParams) ([]types.PBXInboundRoute, int64, error) {
|
|
params = types.DefaultListParams(params)
|
|
|
|
countQuery := `SELECT COUNT(*) FROM pbx_inbound_routes WHERE 1=1`
|
|
listQuery := `SELECT id, tenant_id, name, did_number, destination_type,
|
|
destination_id, destination_data, priority, is_active, trunk_id,
|
|
created_at, updated_at
|
|
FROM pbx_inbound_routes WHERE 1=1`
|
|
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if params.Search != "" {
|
|
countQuery += fmt.Sprintf(" AND (name ILIKE $%d OR did_number ILIKE $%d)", argIdx, argIdx)
|
|
listQuery += fmt.Sprintf(" AND (name ILIKE $%d OR did_number ILIKE $%d)", argIdx, argIdx)
|
|
args = append(args, "%"+params.Search+"%")
|
|
argIdx++
|
|
}
|
|
|
|
var total int64
|
|
if err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil {
|
|
return nil, 0, fmt.Errorf("count query failed: %w", err)
|
|
}
|
|
|
|
listQuery += fmt.Sprintf(" ORDER BY priority ASC, created_at DESC LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
|
|
args = append(args, params.Limit, params.Offset)
|
|
|
|
rows, err := s.pool.Query(ctx, listQuery, args...)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("list query failed: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
routes := make([]types.PBXInboundRoute, 0)
|
|
for rows.Next() {
|
|
var r types.PBXInboundRoute
|
|
if err := rows.Scan(&r.ID, &r.TenantID, &r.Name, &r.DIDNumber, &r.DestinationType,
|
|
&r.DestinationID, &r.DestinationData, &r.Priority, &r.IsActive, &r.TrunkID,
|
|
&r.CreatedAt, &r.UpdatedAt); err != nil {
|
|
return nil, 0, fmt.Errorf("scan failed: %w", err)
|
|
}
|
|
routes = append(routes, r)
|
|
}
|
|
|
|
return routes, total, nil
|
|
}
|
|
|
|
// GetInboundRoute gets an inbound route by ID
|
|
func (s *PBXService) GetInboundRoute(ctx context.Context, id uuid.UUID) (*types.PBXInboundRoute, error) {
|
|
var r types.PBXInboundRoute
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT id, tenant_id, name, did_number, destination_type,
|
|
destination_id, destination_data, priority, is_active, trunk_id,
|
|
created_at, updated_at
|
|
FROM pbx_inbound_routes WHERE id = $1`, id).
|
|
Scan(&r.ID, &r.TenantID, &r.Name, &r.DIDNumber, &r.DestinationType,
|
|
&r.DestinationID, &r.DestinationData, &r.Priority, &r.IsActive, &r.TrunkID,
|
|
&r.CreatedAt, &r.UpdatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// CreateInboundRoute creates a new inbound route
|
|
func (s *PBXService) CreateInboundRoute(ctx context.Context, req *types.PBXInboundRouteCreate) (*types.PBXInboundRoute, error) {
|
|
id := uuid.New()
|
|
now := time.Now().UTC()
|
|
|
|
priority := req.Priority
|
|
if priority == 0 {
|
|
priority = 100
|
|
}
|
|
|
|
_, err := s.pool.Exec(ctx,
|
|
`INSERT INTO pbx_inbound_routes (id, tenant_id, name, did_number, destination_type,
|
|
destination_id, destination_data, priority, is_active, trunk_id, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, $9, $10, $10)`,
|
|
id, req.TenantID, req.Name, nilIfEmpty(req.DIDNumber), req.DestinationType,
|
|
req.DestinationID, nilIfEmpty(req.DestinationData), priority, req.TrunkID, now)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert failed: %w", err)
|
|
}
|
|
|
|
return s.GetInboundRoute(ctx, id)
|
|
}
|
|
|
|
// UpdateInboundRoute updates an inbound route
|
|
func (s *PBXService) UpdateInboundRoute(ctx context.Context, id uuid.UUID, req *types.PBXInboundRouteUpdate) (*types.PBXInboundRoute, error) {
|
|
setClauses := []string{}
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if req.Name != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("name = $%d", argIdx))
|
|
args = append(args, *req.Name)
|
|
argIdx++
|
|
}
|
|
if req.DIDNumber != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("did_number = $%d", argIdx))
|
|
args = append(args, *req.DIDNumber)
|
|
argIdx++
|
|
}
|
|
if req.DestinationType != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("destination_type = $%d", argIdx))
|
|
args = append(args, *req.DestinationType)
|
|
argIdx++
|
|
}
|
|
if req.DestinationID != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("destination_id = $%d", argIdx))
|
|
args = append(args, *req.DestinationID)
|
|
argIdx++
|
|
}
|
|
if req.DestinationData != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("destination_data = $%d", argIdx))
|
|
args = append(args, *req.DestinationData)
|
|
argIdx++
|
|
}
|
|
if req.Priority != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("priority = $%d", argIdx))
|
|
args = append(args, *req.Priority)
|
|
argIdx++
|
|
}
|
|
if req.IsActive != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("is_active = $%d", argIdx))
|
|
args = append(args, *req.IsActive)
|
|
argIdx++
|
|
}
|
|
if req.TrunkID != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("trunk_id = $%d", argIdx))
|
|
args = append(args, *req.TrunkID)
|
|
argIdx++
|
|
}
|
|
|
|
if len(setClauses) == 0 {
|
|
return s.GetInboundRoute(ctx, id)
|
|
}
|
|
|
|
setClauses = append(setClauses, fmt.Sprintf("updated_at = $%d", argIdx))
|
|
args = append(args, time.Now().UTC())
|
|
argIdx++
|
|
|
|
args = append(args, id)
|
|
query := fmt.Sprintf("UPDATE pbx_inbound_routes SET %s WHERE id = $%d",
|
|
join(setClauses, ", "), argIdx)
|
|
|
|
_, err := s.pool.Exec(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("update failed: %w", err)
|
|
}
|
|
|
|
return s.GetInboundRoute(ctx, id)
|
|
}
|
|
|
|
// DeleteInboundRoute deletes an inbound route
|
|
func (s *PBXService) DeleteInboundRoute(ctx context.Context, id uuid.UUID) error {
|
|
_, err := s.pool.Exec(ctx, `DELETE FROM pbx_inbound_routes WHERE id = $1`, id)
|
|
return err
|
|
}
|
|
|
|
// ============================================================================
|
|
// Outbound Routes
|
|
// ============================================================================
|
|
|
|
// ListOutboundRoutes lists outbound routes
|
|
func (s *PBXService) ListOutboundRoutes(ctx context.Context, params types.ListParams) ([]types.PBXOutboundRoute, int64, error) {
|
|
params = types.DefaultListParams(params)
|
|
|
|
countQuery := `SELECT COUNT(*) FROM pbx_outbound_routes WHERE 1=1`
|
|
listQuery := `SELECT id, tenant_id, name, priority, dial_patterns, prepend, prefix,
|
|
strip_digits, override_caller_id, caller_id_name, caller_id_number,
|
|
is_emergency, is_active, created_at, updated_at
|
|
FROM pbx_outbound_routes WHERE 1=1`
|
|
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if params.Search != "" {
|
|
countQuery += fmt.Sprintf(" AND name ILIKE $%d", argIdx)
|
|
listQuery += fmt.Sprintf(" AND name ILIKE $%d", argIdx)
|
|
args = append(args, "%"+params.Search+"%")
|
|
argIdx++
|
|
}
|
|
|
|
var total int64
|
|
if err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil {
|
|
return nil, 0, fmt.Errorf("count query failed: %w", err)
|
|
}
|
|
|
|
listQuery += fmt.Sprintf(" ORDER BY priority ASC LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
|
|
args = append(args, params.Limit, params.Offset)
|
|
|
|
rows, err := s.pool.Query(ctx, listQuery, args...)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("list query failed: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
routes := make([]types.PBXOutboundRoute, 0)
|
|
for rows.Next() {
|
|
var r types.PBXOutboundRoute
|
|
if err := rows.Scan(&r.ID, &r.TenantID, &r.Name, &r.Priority, &r.DialPatterns,
|
|
&r.Prepend, &r.Prefix, &r.StripDigits, &r.OverrideCallerID,
|
|
&r.CallerIDName, &r.CallerIDNumber, &r.IsEmergency, &r.IsActive,
|
|
&r.CreatedAt, &r.UpdatedAt); err != nil {
|
|
return nil, 0, fmt.Errorf("scan failed: %w", err)
|
|
}
|
|
|
|
// Load trunks for this route
|
|
trunkRows, err := s.pool.Query(ctx,
|
|
`SELECT ort.trunk_id, t.name, ort.priority
|
|
FROM pbx_outbound_route_trunks ort
|
|
JOIN pbx_trunks t ON t.id = ort.trunk_id
|
|
WHERE ort.outbound_route_id = $1 ORDER BY ort.priority`, r.ID)
|
|
if err == nil {
|
|
for trunkRows.Next() {
|
|
var rt types.PBXOutboundRouteTrunk
|
|
trunkRows.Scan(&rt.TrunkID, &rt.TrunkName, &rt.Priority)
|
|
r.Trunks = append(r.Trunks, rt)
|
|
}
|
|
trunkRows.Close()
|
|
}
|
|
|
|
routes = append(routes, r)
|
|
}
|
|
|
|
return routes, total, nil
|
|
}
|
|
|
|
// GetOutboundRoute gets an outbound route by ID
|
|
func (s *PBXService) GetOutboundRoute(ctx context.Context, id uuid.UUID) (*types.PBXOutboundRoute, error) {
|
|
var r types.PBXOutboundRoute
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT id, tenant_id, name, priority, dial_patterns, prepend, prefix,
|
|
strip_digits, override_caller_id, caller_id_name, caller_id_number,
|
|
is_emergency, is_active, created_at, updated_at
|
|
FROM pbx_outbound_routes WHERE id = $1`, id).
|
|
Scan(&r.ID, &r.TenantID, &r.Name, &r.Priority, &r.DialPatterns,
|
|
&r.Prepend, &r.Prefix, &r.StripDigits, &r.OverrideCallerID,
|
|
&r.CallerIDName, &r.CallerIDNumber, &r.IsEmergency, &r.IsActive,
|
|
&r.CreatedAt, &r.UpdatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Load trunks
|
|
trunkRows, err := s.pool.Query(ctx,
|
|
`SELECT ort.trunk_id, t.name, ort.priority
|
|
FROM pbx_outbound_route_trunks ort
|
|
JOIN pbx_trunks t ON t.id = ort.trunk_id
|
|
WHERE ort.outbound_route_id = $1 ORDER BY ort.priority`, id)
|
|
if err == nil {
|
|
for trunkRows.Next() {
|
|
var rt types.PBXOutboundRouteTrunk
|
|
trunkRows.Scan(&rt.TrunkID, &rt.TrunkName, &rt.Priority)
|
|
r.Trunks = append(r.Trunks, rt)
|
|
}
|
|
trunkRows.Close()
|
|
}
|
|
|
|
return &r, nil
|
|
}
|
|
|
|
// CreateOutboundRoute creates a new outbound route
|
|
func (s *PBXService) CreateOutboundRoute(ctx context.Context, req *types.PBXOutboundRouteCreate) (*types.PBXOutboundRoute, error) {
|
|
id := uuid.New()
|
|
now := time.Now().UTC()
|
|
|
|
priority := req.Priority
|
|
if priority == 0 {
|
|
priority = 100
|
|
}
|
|
|
|
_, err := s.pool.Exec(ctx,
|
|
`INSERT INTO pbx_outbound_routes (id, tenant_id, name, priority, dial_patterns,
|
|
prepend, prefix, strip_digits, override_caller_id, caller_id_name, caller_id_number,
|
|
is_emergency, is_active, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, true, $13, $13)`,
|
|
id, req.TenantID, req.Name, priority, req.DialPatterns,
|
|
nilIfEmpty(req.Prepend), nilIfEmpty(req.Prefix), req.StripDigits,
|
|
req.OverrideCallerID, nilIfEmpty(req.CallerIDName), nilIfEmpty(req.CallerIDNumber),
|
|
req.IsEmergency, now)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert failed: %w", err)
|
|
}
|
|
|
|
// Insert trunk assignments
|
|
for _, t := range req.Trunks {
|
|
s.pool.Exec(ctx,
|
|
`INSERT INTO pbx_outbound_route_trunks (id, outbound_route_id, trunk_id, priority, created_at)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
uuid.New(), id, t.TrunkID, t.Priority, now)
|
|
}
|
|
|
|
return s.GetOutboundRoute(ctx, id)
|
|
}
|
|
|
|
// UpdateOutboundRoute updates an outbound route
|
|
func (s *PBXService) UpdateOutboundRoute(ctx context.Context, id uuid.UUID, req *types.PBXOutboundRouteUpdate) (*types.PBXOutboundRoute, error) {
|
|
setClauses := []string{}
|
|
args := []interface{}{}
|
|
argIdx := 1
|
|
|
|
if req.Name != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("name = $%d", argIdx))
|
|
args = append(args, *req.Name)
|
|
argIdx++
|
|
}
|
|
if req.Priority != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("priority = $%d", argIdx))
|
|
args = append(args, *req.Priority)
|
|
argIdx++
|
|
}
|
|
if req.DialPatterns != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("dial_patterns = $%d", argIdx))
|
|
args = append(args, req.DialPatterns)
|
|
argIdx++
|
|
}
|
|
if req.Prepend != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("prepend = $%d", argIdx))
|
|
args = append(args, *req.Prepend)
|
|
argIdx++
|
|
}
|
|
if req.Prefix != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("prefix = $%d", argIdx))
|
|
args = append(args, *req.Prefix)
|
|
argIdx++
|
|
}
|
|
if req.StripDigits != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("strip_digits = $%d", argIdx))
|
|
args = append(args, *req.StripDigits)
|
|
argIdx++
|
|
}
|
|
if req.OverrideCallerID != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("override_caller_id = $%d", argIdx))
|
|
args = append(args, *req.OverrideCallerID)
|
|
argIdx++
|
|
}
|
|
if req.CallerIDName != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("caller_id_name = $%d", argIdx))
|
|
args = append(args, *req.CallerIDName)
|
|
argIdx++
|
|
}
|
|
if req.CallerIDNumber != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("caller_id_number = $%d", argIdx))
|
|
args = append(args, *req.CallerIDNumber)
|
|
argIdx++
|
|
}
|
|
if req.IsEmergency != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("is_emergency = $%d", argIdx))
|
|
args = append(args, *req.IsEmergency)
|
|
argIdx++
|
|
}
|
|
if req.IsActive != nil {
|
|
setClauses = append(setClauses, fmt.Sprintf("is_active = $%d", argIdx))
|
|
args = append(args, *req.IsActive)
|
|
argIdx++
|
|
}
|
|
|
|
if len(setClauses) > 0 {
|
|
setClauses = append(setClauses, fmt.Sprintf("updated_at = $%d", argIdx))
|
|
args = append(args, time.Now().UTC())
|
|
argIdx++
|
|
|
|
args = append(args, id)
|
|
query := fmt.Sprintf("UPDATE pbx_outbound_routes SET %s WHERE id = $%d",
|
|
join(setClauses, ", "), argIdx)
|
|
|
|
_, err := s.pool.Exec(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("update failed: %w", err)
|
|
}
|
|
}
|
|
|
|
// Update trunk assignments if provided
|
|
if req.Trunks != nil {
|
|
s.pool.Exec(ctx, `DELETE FROM pbx_outbound_route_trunks WHERE outbound_route_id = $1`, id)
|
|
now := time.Now().UTC()
|
|
for _, t := range req.Trunks {
|
|
s.pool.Exec(ctx,
|
|
`INSERT INTO pbx_outbound_route_trunks (id, outbound_route_id, trunk_id, priority, created_at)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
uuid.New(), id, t.TrunkID, t.Priority, now)
|
|
}
|
|
}
|
|
|
|
return s.GetOutboundRoute(ctx, id)
|
|
}
|
|
|
|
// DeleteOutboundRoute deletes an outbound route
|
|
func (s *PBXService) DeleteOutboundRoute(ctx context.Context, id uuid.UUID) error {
|
|
_, err := s.pool.Exec(ctx, `DELETE FROM pbx_outbound_routes WHERE id = $1`, id)
|
|
return err
|
|
}
|
|
|
|
// ============================================================================
|
|
// System Operations
|
|
// ============================================================================
|
|
|
|
// GetStatus returns PBX system status
|
|
func (s *PBXService) GetStatus(ctx context.Context) (*types.PBXStatus, error) {
|
|
status := &types.PBXStatus{}
|
|
|
|
// Count active trunks
|
|
s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM pbx_trunks WHERE status = 'active'`).Scan(&status.ActiveTrunks)
|
|
|
|
// Count active extensions
|
|
s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM pbx_extensions WHERE is_active = true`).Scan(&status.ActiveExtensions)
|
|
|
|
// Asterisk server status
|
|
if s.asterisk != nil {
|
|
for _, srv := range s.asterisk.Servers() {
|
|
ss := types.PBXServerStatus{Host: srv.Host, Status: "unknown"}
|
|
results := s.asterisk.GetUptime()
|
|
for _, r := range results {
|
|
if r.Host == srv.Host {
|
|
if r.Success {
|
|
ss.Status = "online"
|
|
ss.Uptime = r.Output
|
|
} else {
|
|
ss.Status = "offline"
|
|
}
|
|
}
|
|
}
|
|
status.AsteriskServers = append(status.AsteriskServers, ss)
|
|
}
|
|
}
|
|
|
|
// Kamailio server status
|
|
if s.kamailio != nil {
|
|
for _, srv := range s.kamailio.Servers() {
|
|
status.KamailioServers = append(status.KamailioServers, types.PBXServerStatus{
|
|
Host: srv,
|
|
Status: "configured",
|
|
})
|
|
}
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// Reload triggers a reload on all Asterisk and Kamailio servers
|
|
func (s *PBXService) Reload(ctx context.Context) (*types.PBXReloadResult, error) {
|
|
result := &types.PBXReloadResult{}
|
|
|
|
if s.asterisk != nil {
|
|
astResults := s.asterisk.ReloadAll()
|
|
for _, r := range astResults {
|
|
result.AsteriskResults = append(result.AsteriskResults, types.PBXReloadServer{
|
|
Host: r.Host,
|
|
Success: r.Success,
|
|
Message: r.Output,
|
|
})
|
|
}
|
|
}
|
|
|
|
if s.kamailio != nil {
|
|
kamResults := s.kamailio.ReloadAll()
|
|
for _, r := range kamResults {
|
|
result.KamailioResults = append(result.KamailioResults, types.PBXReloadServer{
|
|
Host: r.Host,
|
|
Success: r.Success,
|
|
Message: r.Output,
|
|
})
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|