mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 17:36:45 +08:00
* MUL-6625: enforce prepaid member seat capacity
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): address review findings
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): close retry and worker races
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): bound lock waits during rollback
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): pin the cross-repo confirm settlement budget
Multica Cloud must wait longer than this worker's whole retry budget before it
treats a member row as one its ledger never recorded; adopting a member whose
confirm is still retrying counts one person as two seats, and an over-estimate
is what asks a customer to buy seats they do not owe.
That budget was only implicit here — 10 attempts of exponential backoff plus a
tick and a lock wait each — and Cloud's horizon had been set from a single
retry. Extract the backoff into retryBackoff, expose SettlementBudget, and
assert the worst case (29m15s) stays inside Cloud's 45m horizon, so raising
maxAttempts or the backoff cap fails review here instead of widening the window
in which Cloud cannot quantify a ledger.
Co-authored-by: multica-agent <[email protected]>
* Revert "fix(seat-capacity): pin the cross-repo confirm settlement budget"
This reverts commit d0ec36f647.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
346 lines
10 KiB
Go
346 lines
10 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: invitation.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const acceptInvitation = `-- name: AcceptInvitation :one
|
|
UPDATE workspace_invitation
|
|
SET status = 'accepted', updated_at = now()
|
|
WHERE id = $1 AND status = 'pending'
|
|
RETURNING id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at
|
|
`
|
|
|
|
func (q *Queries) AcceptInvitation(ctx context.Context, id pgtype.UUID) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, acceptInvitation, id)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createInvitation = `-- name: CreateInvitation :one
|
|
INSERT INTO workspace_invitation (id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, expires_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at
|
|
`
|
|
|
|
type CreateInvitationParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviterID pgtype.UUID `json:"inviter_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
InviteeUserID pgtype.UUID `json:"invitee_user_id"`
|
|
Role string `json:"role"`
|
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
|
}
|
|
|
|
func (q *Queries) CreateInvitation(ctx context.Context, arg CreateInvitationParams) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, createInvitation,
|
|
arg.ID,
|
|
arg.WorkspaceID,
|
|
arg.InviterID,
|
|
arg.InviteeEmail,
|
|
arg.InviteeUserID,
|
|
arg.Role,
|
|
arg.ExpiresAt,
|
|
)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const declineInvitation = `-- name: DeclineInvitation :one
|
|
UPDATE workspace_invitation
|
|
SET status = 'declined', updated_at = now()
|
|
WHERE id = $1 AND status = 'pending'
|
|
RETURNING id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at
|
|
`
|
|
|
|
func (q *Queries) DeclineInvitation(ctx context.Context, id pgtype.UUID) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, declineInvitation, id)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const expireStalePendingInvitations = `-- name: ExpireStalePendingInvitations :many
|
|
UPDATE workspace_invitation
|
|
SET status = 'expired', updated_at = now()
|
|
WHERE workspace_id = $1
|
|
AND invitee_email = $2
|
|
AND status = 'pending'
|
|
AND expires_at <= now()
|
|
RETURNING id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at
|
|
`
|
|
|
|
type ExpireStalePendingInvitationsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
}
|
|
|
|
// Mark any past-due pending invitations for (workspace_id, invitee_email) as expired,
|
|
// so the next CreateInvitation does not collide with the partial unique index
|
|
// idx_invitation_unique_pending (which is WHERE status = 'pending' and cannot
|
|
// itself reference now() in its predicate).
|
|
func (q *Queries) ExpireStalePendingInvitations(ctx context.Context, arg ExpireStalePendingInvitationsParams) ([]WorkspaceInvitation, error) {
|
|
rows, err := q.db.Query(ctx, expireStalePendingInvitations, arg.WorkspaceID, arg.InviteeEmail)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []WorkspaceInvitation{}
|
|
for rows.Next() {
|
|
var i WorkspaceInvitation
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const getInvitation = `-- name: GetInvitation :one
|
|
SELECT id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at FROM workspace_invitation
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetInvitation(ctx context.Context, id pgtype.UUID) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, getInvitation, id)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getPendingInvitationByEmail = `-- name: GetPendingInvitationByEmail :one
|
|
SELECT id, workspace_id, inviter_id, invitee_email, invitee_user_id, role, status, created_at, updated_at, expires_at FROM workspace_invitation
|
|
WHERE workspace_id = $1 AND invitee_email = $2 AND status = 'pending' AND expires_at > now()
|
|
`
|
|
|
|
type GetPendingInvitationByEmailParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
}
|
|
|
|
func (q *Queries) GetPendingInvitationByEmail(ctx context.Context, arg GetPendingInvitationByEmailParams) (WorkspaceInvitation, error) {
|
|
row := q.db.QueryRow(ctx, getPendingInvitationByEmail, arg.WorkspaceID, arg.InviteeEmail)
|
|
var i WorkspaceInvitation
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listPendingInvitationsByWorkspace = `-- name: ListPendingInvitationsByWorkspace :many
|
|
SELECT wi.id, wi.workspace_id, wi.inviter_id, wi.invitee_email, wi.invitee_user_id, wi.role, wi.status, wi.created_at, wi.updated_at, wi.expires_at,
|
|
u.name AS inviter_name,
|
|
u.email AS inviter_email
|
|
FROM workspace_invitation wi
|
|
JOIN "user" u ON u.id = wi.inviter_id
|
|
WHERE wi.workspace_id = $1 AND wi.status = 'pending' AND wi.expires_at > now()
|
|
ORDER BY wi.created_at DESC
|
|
`
|
|
|
|
type ListPendingInvitationsByWorkspaceRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviterID pgtype.UUID `json:"inviter_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
InviteeUserID pgtype.UUID `json:"invitee_user_id"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
|
InviterName string `json:"inviter_name"`
|
|
InviterEmail string `json:"inviter_email"`
|
|
}
|
|
|
|
func (q *Queries) ListPendingInvitationsByWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]ListPendingInvitationsByWorkspaceRow, error) {
|
|
rows, err := q.db.Query(ctx, listPendingInvitationsByWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListPendingInvitationsByWorkspaceRow{}
|
|
for rows.Next() {
|
|
var i ListPendingInvitationsByWorkspaceRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
&i.InviterName,
|
|
&i.InviterEmail,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listPendingInvitationsForUser = `-- name: ListPendingInvitationsForUser :many
|
|
SELECT wi.id, wi.workspace_id, wi.inviter_id, wi.invitee_email, wi.invitee_user_id, wi.role, wi.status, wi.created_at, wi.updated_at, wi.expires_at,
|
|
w.name AS workspace_name,
|
|
u.name AS inviter_name,
|
|
u.email AS inviter_email
|
|
FROM workspace_invitation wi
|
|
JOIN workspace w ON w.id = wi.workspace_id
|
|
JOIN "user" u ON u.id = wi.inviter_id
|
|
WHERE wi.status = 'pending'
|
|
AND (wi.invitee_user_id = $1 OR wi.invitee_email = $2)
|
|
AND wi.expires_at > now()
|
|
ORDER BY wi.created_at DESC
|
|
`
|
|
|
|
type ListPendingInvitationsForUserParams struct {
|
|
InviteeUserID pgtype.UUID `json:"invitee_user_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
}
|
|
|
|
type ListPendingInvitationsForUserRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InviterID pgtype.UUID `json:"inviter_id"`
|
|
InviteeEmail string `json:"invitee_email"`
|
|
InviteeUserID pgtype.UUID `json:"invitee_user_id"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
|
WorkspaceName string `json:"workspace_name"`
|
|
InviterName string `json:"inviter_name"`
|
|
InviterEmail string `json:"inviter_email"`
|
|
}
|
|
|
|
func (q *Queries) ListPendingInvitationsForUser(ctx context.Context, arg ListPendingInvitationsForUserParams) ([]ListPendingInvitationsForUserRow, error) {
|
|
rows, err := q.db.Query(ctx, listPendingInvitationsForUser, arg.InviteeUserID, arg.InviteeEmail)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListPendingInvitationsForUserRow{}
|
|
for rows.Next() {
|
|
var i ListPendingInvitationsForUserRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InviterID,
|
|
&i.InviteeEmail,
|
|
&i.InviteeUserID,
|
|
&i.Role,
|
|
&i.Status,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ExpiresAt,
|
|
&i.WorkspaceName,
|
|
&i.InviterName,
|
|
&i.InviterEmail,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const revokeInvitation = `-- name: RevokeInvitation :execrows
|
|
DELETE FROM workspace_invitation
|
|
WHERE id = $1 AND status = 'pending'
|
|
`
|
|
|
|
func (q *Queries) RevokeInvitation(ctx context.Context, id pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, revokeInvitation, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|