Relay transactional email through Salesforce instead of SMTP (#9)
Jenkins Production Deployment

Reviewed-on: #9
Co-authored-by: Patrick Müller <patrick@mueller-patrick.tech>
Co-committed-by: Patrick Müller <patrick@mueller-patrick.tech>
This commit was merged in pull request #9.
This commit is contained in:
2026-08-31 16:17:59 +00:00
committed by Patrick Müller
parent 3c4f3331d8
commit 449edd6c68
16 changed files with 819 additions and 141 deletions
@@ -204,6 +204,55 @@ redemptionsAdminRouter.post('/:redemptionId/undo', async (req: Request, res: Res
}
});
/**
* @swagger
* /tickets/admin/redemptions/{redemptionId}/resend-confirmation:
* post:
* summary: Resend the redemption confirmation email
* description: Rebuilds the confirmation email from the stored redemption data and sends it again, then records the outcome on the redemption. Intended for redemptions whose original confirmation email failed.
* tags: [tickets-admin]
* parameters:
* - $ref: '#/components/parameters/SessionIdHeader'
* - $ref: '#/components/parameters/SessionKeyHeader'
* - in: path
* name: redemptionId
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: The email was accepted for delivery
* 404:
* description: Unknown redemption
* 409:
* description: Redemption is not active
* 502:
* description: The email relay rejected the send
* 401:
* description: Unauthorized
*/
redemptionsAdminRouter.post('/:redemptionId/resend-confirmation', async (req: Request, res: Response) => {
try {
const result = await RedemptionsAdminService.resendRedemptionConfirmation(Number(req.params.redemptionId));
switch (result) {
case 'SENT':
res.status(200).send({status: 'OK'});
return;
case 'NOT_FOUND':
res.status(404).send({status: 'NOT_FOUND'});
return;
case 'NOT_ACTIVE':
res.status(409).send({status: 'NOT_ACTIVE', message: 'This redemption is not active.'});
return;
case 'FAILED':
res.status(502).send({status: 'SEND_FAILED', message: 'Die E-Mail konnte nicht versendet werden. Bitte später erneut versuchen.'});
return;
}
} catch (e: any) {
sendServerError(res, e);
}
});
/**
* @swagger
* /tickets/admin/vouchers/{code}/history:
@@ -1,5 +1,6 @@
import {NachklangTicketsDB} from '../Tickets.db';
import {getEventTicketState} from '../tickets.capacity';
import {recordConfirmationEmailResult, sendRedemptionConfirmation} from '../tickets.confirmation-email';
import {AuditLogEntry, RedemptionSummary} from '../tickets.interface';
import {isValidEmail} from '../tickets.validation';
@@ -13,7 +14,8 @@ const mapRedemptionRow = (row: any, guests: string[]): RedemptionSummary => ({
contactAddress: row.contact_address,
guestCount: row.guest_count,
guests,
redeemedAt: row.redeemed_at
redeemedAt: row.redeemed_at,
confirmationEmailStatus: row.confirmation_email_status ?? null
});
export interface ListRedemptionsFilter {
@@ -227,6 +229,29 @@ export const editRedemption = async (redemptionId: number, input: EditRedemption
}
};
export type ResendConfirmationResult = 'SENT' | 'FAILED' | 'NOT_FOUND' | 'NOT_ACTIVE';
/**
* Rebuilds and re-sends the redemption confirmation email from the stored
* redemption data, then records the new outcome on the row. Used by the admin
* UI's "resend" action on a redemption whose confirmation email failed. Only
* active redemptions can be resent.
*/
export const resendRedemptionConfirmation = async (redemptionId: number): Promise<ResendConfirmationResult> => {
const redemption = await getRedemption(redemptionId);
if (!redemption) return 'NOT_FOUND';
if (redemption.status !== 'ACTIVE') return 'NOT_ACTIVE';
const sent = await sendRedemptionConfirmation({
eventId: redemption.eventId,
contactName: redemption.contactName,
contactEmail: redemption.contactEmail,
guestNames: redemption.guests
});
await recordConfirmationEmailResult(redemptionId, sent);
return sent ? 'SENT' : 'FAILED';
};
export const getAuditHistory = async (code: string): Promise<AuditLogEntry[]> => {
let conn = await NachklangTicketsDB.getConnection();
try {