Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Humans.Application/DTOs/TicketingBudgetDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,18 @@ public class TicketingWeekProjection
public decimal ProjectedStripeFees { get; init; }
public decimal ProjectedTtFees { get; init; }
}

/// <summary>
/// Aggregated ticket sales for a completed ISO week, used as input to
/// <c>IBudgetService.SyncTicketingActualsAsync</c>. Produced by the ticket
/// side (which owns TicketOrders) and consumed by the budget side (which
/// owns BudgetLineItems / TicketingProjections).
/// </summary>
public record TicketingWeeklyActuals(
LocalDate Monday,
LocalDate Sunday,
string WeekLabel,
int TicketCount,
decimal Revenue,
decimal StripeFees,
decimal TicketTailorFees);
33 changes: 33 additions & 0 deletions src/Humans.Application/Interfaces/IBudgetService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,39 @@ Task UpdateTicketingProjectionAsync(Guid budgetGroupId, LocalDate? startDate, Lo
int initialSalesCount, decimal dailySalesRate, decimal averageTicketPrice, int vatRate,
decimal stripeFeePercent, decimal stripeFeeFixed, decimal ticketTailorFeePercent, Guid actorUserId);

/// <summary>
/// Sync ticket sales actuals (already aggregated per ISO week by the ticket side)
/// into the ticketing budget group. Upserts auto-generated BudgetLineItems for
/// each completed week's revenue and processing fees, refreshes projection
/// parameters (average ticket price, stripe fee %, TicketTailor fee %) from
/// those actuals, and re-materializes projected line items for future weeks.
/// Returns the number of line items created or updated.
/// </summary>
Task<int> SyncTicketingActualsAsync(
Guid budgetYearId,
IReadOnlyList<TicketingWeeklyActuals> weeklyActuals,
CancellationToken ct = default);

/// <summary>
/// Re-materialize projected ticketing line items (no actuals sync). Called
/// after projection parameters change so the projected lines reflect the new inputs.
/// Returns the number of projected line items created.
/// </summary>
Task<int> RefreshTicketingProjectionsAsync(Guid budgetYearId, CancellationToken ct = default);

/// <summary>
/// Compute virtual (non-persisted) weekly ticket projections for future weeks.
/// Used by finance overview pages to display break-even forecasts.
/// </summary>
Task<IReadOnlyList<TicketingWeekProjection>> GetTicketingProjectionEntriesAsync(
Guid budgetGroupId, CancellationToken ct = default);

/// <summary>
/// Compute the total number of tickets sold through completed weeks, derived
/// from the revenue line item notes on an already-loaded ticketing group.
/// </summary>
int GetActualTicketsSold(BudgetGroup ticketingGroup);

// Budget Groups
Task<BudgetGroup> CreateGroupAsync(Guid budgetYearId, string name, bool isRestricted, Guid actorUserId);
Task UpdateGroupAsync(Guid groupId, string name, int sortOrder, bool isRestricted, Guid actorUserId);
Expand Down
17 changes: 17 additions & 0 deletions src/Humans.Application/Interfaces/ICampaignService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Humans.Application.DTOs;
using Humans.Domain.Entities;
using NodaTime;

namespace Humans.Application.Interfaces;

Expand All @@ -10,6 +11,12 @@ public record WaveSendPreview(
int CodesAvailable,
int CodesRemainingAfterSend);

/// <summary>
/// A discount-code redemption discovered by ticket sync — code string and the
/// instant the redeeming ticket order was purchased.
/// </summary>
public record DiscountCodeRedemption(string Code, Instant RedeemedAt);

public interface ICampaignService
{
Task<Campaign> CreateAsync(string title, string? description,
Expand All @@ -31,4 +38,14 @@ Task<bool> UpdateAsync(Guid id, string title, string? description,
Task<int> SendWaveAsync(Guid campaignId, Guid teamId, CancellationToken ct = default);
Task ResendToGrantAsync(Guid grantId, CancellationToken ct = default);
Task RetryAllFailedAsync(Guid campaignId, CancellationToken ct = default);

/// <summary>
/// Marks campaign grants as redeemed based on discovered discount-code redemptions.
/// Matches codes case-insensitively against active/completed campaigns' unredeemed grants.
/// When a code matches grants in multiple campaigns, the most recently created campaign wins.
/// Returns the number of grants marked as redeemed.
/// </summary>
Task<int> MarkGrantsRedeemedAsync(
IReadOnlyCollection<DiscountCodeRedemption> redemptions,
CancellationToken ct = default);
}
21 changes: 21 additions & 0 deletions src/Humans.Application/Interfaces/IEmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,25 @@ Task SendWorkspaceCredentialsAsync(
string tempPassword,
string? culture = null,
CancellationToken cancellationToken = default);

/// <summary>
/// Enqueue a campaign code email to a recipient. Campaign content (subject and
/// markdown body with {{Code}}/{{Name}} placeholders) is rendered and wrapped in
/// the system email template by the outbox service, then linked to the grant
/// via <see cref="CampaignCodeEmailRequest.CampaignGrantId"/> for status tracking.
/// </summary>
Task SendCampaignCodeAsync(CampaignCodeEmailRequest request, CancellationToken cancellationToken = default);
}

/// <summary>
/// Payload for enqueuing a campaign-code email.
/// </summary>
public record CampaignCodeEmailRequest(
Guid UserId,
Guid CampaignGrantId,
string RecipientEmail,
string RecipientName,
string Subject,
string MarkdownBody,
string Code,
string? ReplyTo);
44 changes: 44 additions & 0 deletions src/Humans.Application/Interfaces/ITicketQueryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,52 @@ Task<WhoHasntBoughtResult> GetWhoHasntBoughtAsync(
/// Gets ticket order summaries for a specific user (as buyer), ordered by most recent first.
/// </summary>
Task<List<UserTicketOrderSummary>> GetUserTicketOrderSummariesAsync(Guid userId);

/// <summary>
/// Returns whether a user holds a valid ticket for the current sync's vendor event.
/// Checks paid orders matched to the user, then falls back to valid/checked-in attendees.
/// Used by profile services to compute account-deletion / event-hold dates without
/// touching ticket tables directly.
/// </summary>
Task<bool> HasCurrentEventTicketAsync(Guid userId, CancellationToken ct = default);

/// <summary>
/// Returns ticket data for a user's GDPR data export — matched orders (as buyer)
/// and matched attendee records. Shape matches the existing profile export JSON
/// so GDPR exports stay stable after the service-ownership refactor.
/// </summary>
Task<UserTicketExportData> GetUserTicketExportDataAsync(Guid userId, CancellationToken ct = default);
}

/// <summary>
/// Ticket data for a user's GDPR data export.
/// </summary>
public record UserTicketExportData(
IReadOnlyList<UserTicketOrderExportRow> Orders,
IReadOnlyList<UserTicketAttendeeExportRow> Attendees);

/// <summary>
/// A single ticket order row in the user data export.
/// </summary>
public record UserTicketOrderExportRow(
string? BuyerName,
string? BuyerEmail,
decimal TotalAmount,
string Currency,
string PaymentStatus,
string? DiscountCode,
Instant PurchasedAt);

/// <summary>
/// A single ticket attendee row in the user data export.
/// </summary>
public record UserTicketAttendeeExportRow(
string? AttendeeName,
string? AttendeeEmail,
string? TicketTypeName,
decimal Price,
string Status);

/// <summary>
/// Summary of a ticket order for display on user-facing pages.
/// </summary>
Expand Down
Loading
Loading