Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

237 lines
6.3 KiB
JavaScript

const { randomUUID } = require('node:crypto');
function getApiKey() {
return process.env.WORKFLOW_HARNESS_API_KEY || process.env.ALGA_API_KEY || '';
}
async function pickOne(ctx, { label, sql, params }) {
const rows = await ctx.db.query(sql, params);
if (!rows.length) throw new Error(`Fixture requires ${label} in DB (tenant=${ctx.config.tenantId}).`);
return rows[0];
}
async function ensureTenantEmailSettings(ctx) {
const tenantId = ctx.config.tenantId;
const fixtureProviderConfigs = [
{
providerId: 'fixture-smtp',
providerType: 'smtp',
isEnabled: true,
config: {
host: 'imap-test-server',
port: 3025,
secure: false,
username: 'imap_user',
password: 'imap_pass',
from: 'no-reply@example.com',
rejectUnauthorized: false
}
}
];
const existing = await ctx.db.query(
`
select
id,
default_from_domain,
ticketing_from_email,
custom_domains,
email_provider,
provider_configs,
fallback_enabled,
tracking_enabled,
max_daily_emails,
updated_at
from tenant_email_settings
where tenant = $1
order by id asc
limit 1
`,
[tenantId]
);
if (existing.length) {
const row = existing[0];
await ctx.dbWrite.query(
`
update tenant_email_settings
set
default_from_domain = $2,
ticketing_from_email = $3,
custom_domains = $4::json,
email_provider = $5,
provider_configs = $6::json,
fallback_enabled = $7,
tracking_enabled = $8,
max_daily_emails = $9,
updated_at = now()
where id = $1
`,
[
row.id,
'example.com',
null,
JSON.stringify([]),
'smtp',
JSON.stringify(fixtureProviderConfigs),
true,
false,
null
]
);
ctx.onCleanup(async () => {
await ctx.dbWrite.query(
`
update tenant_email_settings
set
default_from_domain = $2,
ticketing_from_email = $3,
custom_domains = $4::json,
email_provider = $5,
provider_configs = $6::json,
fallback_enabled = $7,
tracking_enabled = $8,
max_daily_emails = $9,
updated_at = $10
where id = $1
`,
[
row.id,
row.default_from_domain,
row.ticketing_from_email,
JSON.stringify(row.custom_domains ?? []),
row.email_provider,
JSON.stringify(row.provider_configs ?? []),
row.fallback_enabled,
row.tracking_enabled,
row.max_daily_emails,
row.updated_at
]
);
});
return;
}
const inserted = await ctx.dbWrite.query(
`
insert into tenant_email_settings (
tenant,
default_from_domain,
ticketing_from_email,
custom_domains,
email_provider,
provider_configs,
fallback_enabled,
tracking_enabled,
max_daily_emails,
created_at,
updated_at
)
values ($1, $2, $3, $4::json, $5, $6::json, $7, $8, $9, now(), now())
returning id
`,
[
tenantId,
'example.com',
null,
JSON.stringify([]),
'smtp',
JSON.stringify(fixtureProviderConfigs),
true,
false,
null
]
);
const insertedId = inserted[0]?.id;
ctx.onCleanup(async () => {
if (insertedId) {
await ctx.dbWrite.query(`delete from tenant_email_settings where id = $1`, [insertedId]);
}
});
}
module.exports = async function run(ctx) {
const apiKey = getApiKey();
if (!apiKey) {
throw new Error('Missing WORKFLOW_HARNESS_API_KEY (or ALGA_API_KEY) for /api/v1 calls.');
}
const tenantId = ctx.config.tenantId;
await ensureTenantEmailSettings(ctx);
const client = await pickOne(ctx, {
label: 'a client',
sql: `select client_id from clients where tenant = $1 order by created_at asc limit 1`,
params: [tenantId]
});
const board = await pickOne(ctx, {
label: 'a ticket board',
sql: `select board_id from boards where tenant = $1 order by is_default desc, display_order asc limit 1`,
params: [tenantId]
});
const status = await pickOne(ctx, {
label: 'a ticket status',
sql: `select status_id from statuses where tenant = $1 and board_id = $2 and status_type = 'ticket' order by is_default desc, order_number asc limit 1`,
params: [tenantId, board.board_id]
});
const priority = await pickOne(ctx, {
label: 'a ticket priority',
sql: `select priority_id from priorities where tenant = $1 order by order_number desc limit 1`,
params: [tenantId]
});
const title = `Fixture after-hours queue ${randomUUID()}`;
const createRes = await ctx.http.request('/api/v1/tickets', {
method: 'POST',
headers: { 'x-api-key': apiKey },
json: {
title,
client_id: client.client_id,
board_id: board.board_id,
status_id: status.status_id,
priority_id: priority.priority_id
}
});
const ticketId = createRes.json?.data?.ticket_id;
if (!ticketId) throw new Error('Ticket create response missing data.ticket_id');
ctx.onCleanup(async () => {
await ctx.http.request(`/api/v1/tickets/${ticketId}`, {
method: 'DELETE',
headers: { 'x-api-key': apiKey }
});
});
await ctx.http.request('/api/workflow/events', {
method: 'POST',
json: {
eventName: 'TICKET_QUEUE_CHANGED',
correlationKey: ticketId,
payloadSchemaRef: 'payload.TicketQueueChanged.v1',
payload: {
ticketId,
previousBoardId: randomUUID(),
newBoardId: board.board_id,
fixtureAfterHoursBoardId: board.board_id,
fixtureOnCallEmail: 'oncall@example.com'
}
}
});
const runRow = await ctx.waitForRun({ startedAfter: ctx.triggerStartedAt });
if (runRow.status !== 'SUCCEEDED') {
const steps = await ctx.getRunSteps(runRow.run_id);
throw new Error(`Expected run SUCCEEDED, got ${runRow.status}. Steps: ${JSON.stringify(ctx.summarizeSteps(steps))}`);
}
const steps = await ctx.getRunSteps(runRow.run_id);
const emailStep = steps.find((s) => s.definition_step_id === 'send-email');
ctx.expect.ok(emailStep && emailStep.status === 'SUCCEEDED', 'expected send-email step SUCCEEDED');
};