mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-24 08:33:41 +01:00
feat(guild): separate scout invitations into guild_invites table
Scout invitations were stored in guild_applications with type 'invited', forcing the scout list response to use charID as the invitation ID — a known hack that made CancelGuildScout semantically incorrect. Introduce a dedicated guild_invites table (migration 0012) with a serial PK. The scout list now returns real invite IDs and actual InvitedAt timestamps. CancelGuildScout cancels by PK. AcceptInvite and DeclineInvite operate on guild_invites while player-applied applications remain in guild_applications unchanged.
This commit is contained in:
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Guild scout invitations now use a dedicated `guild_invites` table (migration `0012_guild_invites`), giving each invitation a real serial PK; the scout list response now returns accurate invite IDs and timestamps, and `CancelGuildScout` uses the correct PK instead of the character ID.
|
||||||
- Event Tent (campaign) system: code redemption, stamp tracking, reward claiming, and quest gating for special event quests, backed by 8 new database tables and seeded with community-researched live-game campaign data ([#182](https://github.com/Mezeporta/Erupe/pull/182), by stratick).
|
- Event Tent (campaign) system: code redemption, stamp tracking, reward claiming, and quest gating for special event quests, backed by 8 new database tables and seeded with community-researched live-game campaign data ([#182](https://github.com/Mezeporta/Erupe/pull/182), by stratick).
|
||||||
- Database migration `0010_campaign` (campaigns, campaign_categories, campaign_category_links, campaign_rewards, campaign_rewards_claimed, campaign_state, campaign_codes, campaign_quest).
|
- Database migration `0010_campaign` (campaigns, campaign_categories, campaign_category_links, campaign_rewards, campaign_rewards_claimed, campaign_state, campaign_codes, campaign_quest).
|
||||||
- JSON Hunting Road config: `bin/rengoku_data.json` is now supported as a human-readable alternative to the opaque `rengoku_data.bin` — the server assembles and ECD-encrypts the binary at startup, with `.bin` used as a fallback ([#173](https://github.com/Mezeporta/Erupe/issues/173)).
|
- JSON Hunting Road config: `bin/rengoku_data.json` is now supported as a human-readable alternative to the opaque `rengoku_data.bin` — the server assembles and ECD-encrypts the binary at startup, with `.bin` used as a fallback ([#173](https://github.com/Mezeporta/Erupe/issues/173)).
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.server.guildRepo.CancelInvitation(guild.ID, pkt.InvitationID)
|
err = s.server.guildRepo.CancelInvite(pkt.InvitationID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
|
||||||
@@ -123,28 +123,25 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chars, err := s.server.guildRepo.ListInvitedCharacters(guildInfo.ID)
|
invites, err := s.server.guildRepo.ListInvites(guildInfo.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to retrieve scouted characters", zap.Error(err))
|
s.logger.Error("failed to retrieve scout invites", zap.Error(err))
|
||||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bf := byteframe.NewByteFrame()
|
bf := byteframe.NewByteFrame()
|
||||||
bf.SetBE()
|
bf.SetBE()
|
||||||
bf.WriteUint32(uint32(len(chars)))
|
bf.WriteUint32(uint32(len(invites)))
|
||||||
|
|
||||||
for _, sc := range chars {
|
for _, inv := range invites {
|
||||||
// This seems to be used as a unique ID for the invitation sent
|
bf.WriteUint32(inv.ID)
|
||||||
// we can just use the charID and then filter on guild_id+charID when performing operations
|
bf.WriteUint32(inv.ActorID)
|
||||||
// this might be a problem later with mails sent referencing IDs but we'll see.
|
bf.WriteUint32(inv.CharID)
|
||||||
bf.WriteUint32(sc.CharID)
|
bf.WriteUint32(uint32(inv.InvitedAt.Unix()))
|
||||||
bf.WriteUint32(sc.ActorID)
|
bf.WriteUint16(inv.HR)
|
||||||
bf.WriteUint32(sc.CharID)
|
bf.WriteUint16(inv.GR)
|
||||||
bf.WriteUint32(uint32(TimeAdjusted().Unix()))
|
bf.WriteBytes(stringsupport.PaddedString(inv.Name, 32, true))
|
||||||
bf.WriteUint16(sc.HR)
|
|
||||||
bf.WriteUint16(sc.GR)
|
|
||||||
bf.WriteBytes(stringsupport.PaddedString(sc.Name, 32, true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func TestAnswerGuildScout_Accept(t *testing.T) {
|
|||||||
server := createMockServer()
|
server := createMockServer()
|
||||||
mailMock := &mockMailRepo{}
|
mailMock := &mockMailRepo{}
|
||||||
guildMock := &mockGuildRepo{
|
guildMock := &mockGuildRepo{
|
||||||
application: &GuildApplication{GuildID: 10, CharID: 1},
|
hasInviteResult: true,
|
||||||
}
|
}
|
||||||
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
|
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
|
||||||
guildMock.guild.LeaderCharID = 50
|
guildMock.guild.LeaderCharID = 50
|
||||||
@@ -29,8 +29,8 @@ func TestAnswerGuildScout_Accept(t *testing.T) {
|
|||||||
|
|
||||||
handleMsgMhfAnswerGuildScout(session, pkt)
|
handleMsgMhfAnswerGuildScout(session, pkt)
|
||||||
|
|
||||||
if guildMock.acceptedCharID != 1 {
|
if guildMock.acceptInviteCharID != 1 {
|
||||||
t.Errorf("AcceptApplication charID = %d, want 1", guildMock.acceptedCharID)
|
t.Errorf("AcceptInvite charID = %d, want 1", guildMock.acceptInviteCharID)
|
||||||
}
|
}
|
||||||
if len(mailMock.sentMails) != 2 {
|
if len(mailMock.sentMails) != 2 {
|
||||||
t.Fatalf("Expected 2 mails (self + leader), got %d", len(mailMock.sentMails))
|
t.Fatalf("Expected 2 mails (self + leader), got %d", len(mailMock.sentMails))
|
||||||
@@ -47,7 +47,7 @@ func TestAnswerGuildScout_Decline(t *testing.T) {
|
|||||||
server := createMockServer()
|
server := createMockServer()
|
||||||
mailMock := &mockMailRepo{}
|
mailMock := &mockMailRepo{}
|
||||||
guildMock := &mockGuildRepo{
|
guildMock := &mockGuildRepo{
|
||||||
application: &GuildApplication{GuildID: 10, CharID: 1},
|
hasInviteResult: true,
|
||||||
}
|
}
|
||||||
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
|
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
|
||||||
guildMock.guild.LeaderCharID = 50
|
guildMock.guild.LeaderCharID = 50
|
||||||
@@ -64,8 +64,8 @@ func TestAnswerGuildScout_Decline(t *testing.T) {
|
|||||||
|
|
||||||
handleMsgMhfAnswerGuildScout(session, pkt)
|
handleMsgMhfAnswerGuildScout(session, pkt)
|
||||||
|
|
||||||
if guildMock.rejectedCharID != 1 {
|
if guildMock.declineInviteCharID != 1 {
|
||||||
t.Errorf("RejectApplication charID = %d, want 1", guildMock.rejectedCharID)
|
t.Errorf("DeclineInvite charID = %d, want 1", guildMock.declineInviteCharID)
|
||||||
}
|
}
|
||||||
if len(mailMock.sentMails) != 2 {
|
if len(mailMock.sentMails) != 2 {
|
||||||
t.Fatalf("Expected 2 mails (self + leader), got %d", len(mailMock.sentMails))
|
t.Fatalf("Expected 2 mails (self + leader), got %d", len(mailMock.sentMails))
|
||||||
@@ -101,7 +101,7 @@ func TestAnswerGuildScout_ApplicationMissing(t *testing.T) {
|
|||||||
server := createMockServer()
|
server := createMockServer()
|
||||||
mailMock := &mockMailRepo{}
|
mailMock := &mockMailRepo{}
|
||||||
guildMock := &mockGuildRepo{
|
guildMock := &mockGuildRepo{
|
||||||
application: nil, // no application found
|
hasInviteResult: false, // no invite found
|
||||||
}
|
}
|
||||||
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
|
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
|
||||||
guildMock.guild.LeaderCharID = 50
|
guildMock.guild.LeaderCharID = 50
|
||||||
@@ -134,7 +134,7 @@ func TestAnswerGuildScout_MailError(t *testing.T) {
|
|||||||
server := createMockServer()
|
server := createMockServer()
|
||||||
mailMock := &mockMailRepo{sendErr: errNotFound}
|
mailMock := &mockMailRepo{sendErr: errNotFound}
|
||||||
guildMock := &mockGuildRepo{
|
guildMock := &mockGuildRepo{
|
||||||
application: &GuildApplication{GuildID: 10, CharID: 1},
|
hasInviteResult: true,
|
||||||
}
|
}
|
||||||
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
|
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
|
||||||
guildMock.guild.LeaderCharID = 50
|
guildMock.guild.LeaderCharID = 50
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
@@ -270,8 +271,9 @@ func (r *GuildRepository) CreateApplication(guildID, charID, actorID uint32, app
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateApplicationWithMail atomically creates an application and sends a notification mail.
|
// CreateInviteWithMail atomically inserts a scout invitation into guild_invites
|
||||||
func (r *GuildRepository) CreateApplicationWithMail(guildID, charID, actorID uint32, appType GuildApplicationType, mailSenderID, mailRecipientID uint32, mailSubject, mailBody string) error {
|
// and sends a notification mail to the target character.
|
||||||
|
func (r *GuildRepository) CreateInviteWithMail(guildID, charID, actorID uint32, mailSenderID, mailRecipientID uint32, mailSubject, mailBody string) error {
|
||||||
tx, err := r.db.BeginTxx(context.Background(), nil)
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -279,8 +281,8 @@ func (r *GuildRepository) CreateApplicationWithMail(guildID, charID, actorID uin
|
|||||||
defer func() { _ = tx.Rollback() }()
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
if _, err := tx.Exec(
|
if _, err := tx.Exec(
|
||||||
`INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type) VALUES ($1, $2, $3, $4)`,
|
`INSERT INTO guild_invites (guild_id, character_id, actor_id) VALUES ($1, $2, $3)`,
|
||||||
guildID, charID, actorID, appType); err != nil {
|
guildID, charID, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := tx.Exec(mailInsertQuery, mailSenderID, mailRecipientID, mailSubject, mailBody, 0, 0, true, false); err != nil {
|
if _, err := tx.Exec(mailInsertQuery, mailSenderID, mailRecipientID, mailSubject, mailBody, 0, 0, true, false); err != nil {
|
||||||
@@ -289,11 +291,55 @@ func (r *GuildRepository) CreateApplicationWithMail(guildID, charID, actorID uin
|
|||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelInvitation removes an invitation for a character.
|
// HasInvite reports whether a pending scout invitation exists for the character in the guild.
|
||||||
func (r *GuildRepository) CancelInvitation(guildID, charID uint32) error {
|
func (r *GuildRepository) HasInvite(guildID, charID uint32) (bool, error) {
|
||||||
|
var n int
|
||||||
|
err := r.db.QueryRow(
|
||||||
|
`SELECT 1 FROM guild_invites WHERE guild_id = $1 AND character_id = $2`,
|
||||||
|
guildID, charID,
|
||||||
|
).Scan(&n)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelInvite removes a scout invitation by its primary key.
|
||||||
|
func (r *GuildRepository) CancelInvite(inviteID uint32) error {
|
||||||
|
_, err := r.db.Exec(`DELETE FROM guild_invites WHERE id = $1`, inviteID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptInvite removes the scout invitation and adds the character to the guild atomically.
|
||||||
|
func (r *GuildRepository) AcceptInvite(guildID, charID uint32) error {
|
||||||
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
if _, err := tx.Exec(
|
||||||
|
`DELETE FROM guild_invites WHERE guild_id = $1 AND character_id = $2`,
|
||||||
|
guildID, charID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(`
|
||||||
|
INSERT INTO guild_characters (guild_id, character_id, order_index)
|
||||||
|
VALUES ($1, $2, (SELECT MAX(order_index) + 1 FROM guild_characters WHERE guild_id = $1))
|
||||||
|
`, guildID, charID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeclineInvite removes a scout invitation without joining the guild.
|
||||||
|
func (r *GuildRepository) DeclineInvite(guildID, charID uint32) error {
|
||||||
_, err := r.db.Exec(
|
_, err := r.db.Exec(
|
||||||
`DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'invited'`,
|
`DELETE FROM guild_invites WHERE guild_id = $1 AND character_id = $2`,
|
||||||
charID, guildID,
|
guildID, charID,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -433,34 +479,39 @@ func (r *GuildRepository) SetRecruiter(charID uint32, allowed bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScoutedCharacter represents an invited character in the scout list.
|
// GuildInvite represents a pending scout invitation with the target character's info.
|
||||||
type ScoutedCharacter struct {
|
type GuildInvite struct {
|
||||||
CharID uint32 `db:"id"`
|
ID uint32 `db:"id"`
|
||||||
Name string `db:"name"`
|
GuildID uint32 `db:"guild_id"`
|
||||||
HR uint16 `db:"hr"`
|
CharID uint32 `db:"character_id"`
|
||||||
GR uint16 `db:"gr"`
|
ActorID uint32 `db:"actor_id"`
|
||||||
ActorID uint32 `db:"actor_id"`
|
InvitedAt time.Time `db:"created_at"`
|
||||||
|
HR uint16 `db:"hr"`
|
||||||
|
GR uint16 `db:"gr"`
|
||||||
|
Name string `db:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListInvitedCharacters returns all characters with pending guild invitations.
|
// ListInvites returns all pending scout invitations for a guild, including
|
||||||
func (r *GuildRepository) ListInvitedCharacters(guildID uint32) ([]*ScoutedCharacter, error) {
|
// the target character's HR, GR, and name.
|
||||||
|
func (r *GuildRepository) ListInvites(guildID uint32) ([]*GuildInvite, error) {
|
||||||
rows, err := r.db.Queryx(`
|
rows, err := r.db.Queryx(`
|
||||||
SELECT c.id, c.name, c.hr, c.gr, ga.actor_id
|
SELECT gi.id, gi.guild_id, gi.character_id, gi.actor_id, gi.created_at,
|
||||||
FROM guild_applications ga
|
c.hr, c.gr, c.name
|
||||||
JOIN characters c ON c.id = ga.character_id
|
FROM guild_invites gi
|
||||||
WHERE ga.guild_id = $1 AND ga.application_type = 'invited'
|
JOIN characters c ON c.id = gi.character_id
|
||||||
|
WHERE gi.guild_id = $1
|
||||||
`, guildID)
|
`, guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func() { _ = rows.Close() }()
|
defer func() { _ = rows.Close() }()
|
||||||
var chars []*ScoutedCharacter
|
var invites []*GuildInvite
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
sc := &ScoutedCharacter{}
|
inv := &GuildInvite{}
|
||||||
if err := rows.StructScan(sc); err != nil {
|
if err := rows.StructScan(inv); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
chars = append(chars, sc)
|
invites = append(invites, inv)
|
||||||
}
|
}
|
||||||
return chars, nil
|
return invites, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -533,66 +533,77 @@ func TestAddMemberDailyRP(t *testing.T) {
|
|||||||
|
|
||||||
// --- Invitation / Scout tests ---
|
// --- Invitation / Scout tests ---
|
||||||
|
|
||||||
func TestCancelInvitation(t *testing.T) {
|
func TestCancelInvite(t *testing.T) {
|
||||||
repo, db, guildID, leaderID := setupGuildRepo(t)
|
repo, db, guildID, leaderID := setupGuildRepo(t)
|
||||||
|
|
||||||
user2 := CreateTestUser(t, db, "invite_user")
|
user2 := CreateTestUser(t, db, "invite_user")
|
||||||
char2 := CreateTestCharacter(t, db, user2, "Invited")
|
char2 := CreateTestCharacter(t, db, user2, "Invited")
|
||||||
|
|
||||||
if err := repo.CreateApplication(guildID, char2, leaderID, GuildApplicationTypeInvited); err != nil {
|
if err := repo.CreateInviteWithMail(guildID, char2, leaderID, leaderID, char2, "Invite", "body"); err != nil {
|
||||||
t.Fatalf("CreateApplication (invited) failed: %v", err)
|
t.Fatalf("CreateInviteWithMail failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repo.CancelInvitation(guildID, char2); err != nil {
|
invites, err := repo.ListInvites(guildID)
|
||||||
t.Fatalf("CancelInvitation failed: %v", err)
|
if err != nil || len(invites) != 1 {
|
||||||
|
t.Fatalf("Expected 1 invite, got %d (err: %v)", len(invites), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
has, err := repo.HasApplication(guildID, char2)
|
if err := repo.CancelInvite(invites[0].ID); err != nil {
|
||||||
|
t.Fatalf("CancelInvite failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
has, err := repo.HasInvite(guildID, char2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("HasApplication failed: %v", err)
|
t.Fatalf("HasInvite failed: %v", err)
|
||||||
}
|
}
|
||||||
if has {
|
if has {
|
||||||
t.Error("Expected no application after cancellation")
|
t.Error("Expected no invite after cancellation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListInvitedCharacters(t *testing.T) {
|
func TestListInvites(t *testing.T) {
|
||||||
repo, db, guildID, leaderID := setupGuildRepo(t)
|
repo, db, guildID, leaderID := setupGuildRepo(t)
|
||||||
|
|
||||||
user2 := CreateTestUser(t, db, "scout_user")
|
user2 := CreateTestUser(t, db, "scout_user")
|
||||||
char2 := CreateTestCharacter(t, db, user2, "Scouted")
|
char2 := CreateTestCharacter(t, db, user2, "Scouted")
|
||||||
|
|
||||||
if err := repo.CreateApplication(guildID, char2, leaderID, GuildApplicationTypeInvited); err != nil {
|
if err := repo.CreateInviteWithMail(guildID, char2, leaderID, leaderID, char2, "Invite", "body"); err != nil {
|
||||||
t.Fatalf("CreateApplication failed: %v", err)
|
t.Fatalf("CreateInviteWithMail failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chars, err := repo.ListInvitedCharacters(guildID)
|
invites, err := repo.ListInvites(guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ListInvitedCharacters failed: %v", err)
|
t.Fatalf("ListInvites failed: %v", err)
|
||||||
}
|
}
|
||||||
if len(chars) != 1 {
|
if len(invites) != 1 {
|
||||||
t.Fatalf("Expected 1 invited character, got %d", len(chars))
|
t.Fatalf("Expected 1 invite, got %d", len(invites))
|
||||||
}
|
}
|
||||||
if chars[0].CharID != char2 {
|
if invites[0].CharID != char2 {
|
||||||
t.Errorf("Expected char ID %d, got %d", char2, chars[0].CharID)
|
t.Errorf("Expected char ID %d, got %d", char2, invites[0].CharID)
|
||||||
}
|
}
|
||||||
if chars[0].Name != "Scouted" {
|
if invites[0].Name != "Scouted" {
|
||||||
t.Errorf("Expected name 'Scouted', got %q", chars[0].Name)
|
t.Errorf("Expected name 'Scouted', got %q", invites[0].Name)
|
||||||
}
|
}
|
||||||
if chars[0].ActorID != leaderID {
|
if invites[0].ActorID != leaderID {
|
||||||
t.Errorf("Expected actor ID %d, got %d", leaderID, chars[0].ActorID)
|
t.Errorf("Expected actor ID %d, got %d", leaderID, invites[0].ActorID)
|
||||||
|
}
|
||||||
|
if invites[0].ID == 0 {
|
||||||
|
t.Error("Expected non-zero invite ID")
|
||||||
|
}
|
||||||
|
if invites[0].InvitedAt.IsZero() {
|
||||||
|
t.Error("Expected non-zero InvitedAt timestamp")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListInvitedCharactersEmpty(t *testing.T) {
|
func TestListInvitesEmpty(t *testing.T) {
|
||||||
repo, _, guildID, _ := setupGuildRepo(t)
|
repo, _, guildID, _ := setupGuildRepo(t)
|
||||||
|
|
||||||
chars, err := repo.ListInvitedCharacters(guildID)
|
invites, err := repo.ListInvites(guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ListInvitedCharacters failed: %v", err)
|
t.Fatalf("ListInvites failed: %v", err)
|
||||||
}
|
}
|
||||||
if len(chars) != 0 {
|
if len(invites) != 0 {
|
||||||
t.Errorf("Expected 0 invited characters, got %d", len(chars))
|
t.Errorf("Expected 0 invites, got %d", len(invites))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1486,28 +1497,26 @@ func TestDisbandCleansUpAlliance(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CreateApplicationWithMail ---
|
// --- CreateInviteWithMail ---
|
||||||
|
|
||||||
func TestCreateApplicationWithMail(t *testing.T) {
|
func TestCreateInviteWithMail(t *testing.T) {
|
||||||
repo, db, guildID, leaderID := setupGuildRepo(t)
|
repo, db, guildID, leaderID := setupGuildRepo(t)
|
||||||
|
|
||||||
user2 := CreateTestUser(t, db, "scout_mail_user")
|
user2 := CreateTestUser(t, db, "scout_mail_user")
|
||||||
char2 := CreateTestCharacter(t, db, user2, "ScoutTarget")
|
char2 := CreateTestCharacter(t, db, user2, "ScoutTarget")
|
||||||
|
|
||||||
err := repo.CreateApplicationWithMail(
|
err := repo.CreateInviteWithMail(guildID, char2, leaderID, leaderID, char2, "Guild Invite", "You have been invited!")
|
||||||
guildID, char2, leaderID, GuildApplicationTypeInvited,
|
|
||||||
leaderID, char2, "Guild Invite", "You have been invited!")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CreateApplicationWithMail failed: %v", err)
|
t.Fatalf("CreateInviteWithMail failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify application was created
|
// Verify invite was created
|
||||||
has, err := repo.HasApplication(guildID, char2)
|
has, err := repo.HasInvite(guildID, char2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("HasApplication failed: %v", err)
|
t.Fatalf("HasInvite failed: %v", err)
|
||||||
}
|
}
|
||||||
if !has {
|
if !has {
|
||||||
t.Error("Expected application to exist after CreateApplicationWithMail")
|
t.Error("Expected invite to exist after CreateInviteWithMail")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify mail was sent
|
// Verify mail was sent
|
||||||
|
|||||||
@@ -64,8 +64,11 @@ type GuildRepo interface {
|
|||||||
RemoveCharacter(charID uint32) error
|
RemoveCharacter(charID uint32) error
|
||||||
AcceptApplication(guildID, charID uint32) error
|
AcceptApplication(guildID, charID uint32) error
|
||||||
CreateApplication(guildID, charID, actorID uint32, appType GuildApplicationType) error
|
CreateApplication(guildID, charID, actorID uint32, appType GuildApplicationType) error
|
||||||
CreateApplicationWithMail(guildID, charID, actorID uint32, appType GuildApplicationType, mailSenderID, mailRecipientID uint32, mailSubject, mailBody string) error
|
CreateInviteWithMail(guildID, charID, actorID uint32, mailSenderID, mailRecipientID uint32, mailSubject, mailBody string) error
|
||||||
CancelInvitation(guildID, charID uint32) error
|
HasInvite(guildID, charID uint32) (bool, error)
|
||||||
|
CancelInvite(inviteID uint32) error
|
||||||
|
AcceptInvite(guildID, charID uint32) error
|
||||||
|
DeclineInvite(guildID, charID uint32) error
|
||||||
RejectApplication(guildID, charID uint32) error
|
RejectApplication(guildID, charID uint32) error
|
||||||
ArrangeCharacters(charIDs []uint32) error
|
ArrangeCharacters(charIDs []uint32) error
|
||||||
GetApplication(guildID, charID uint32, appType GuildApplicationType) (*GuildApplication, error)
|
GetApplication(guildID, charID uint32, appType GuildApplicationType) (*GuildApplication, error)
|
||||||
@@ -120,7 +123,7 @@ type GuildRepo interface {
|
|||||||
CountGuildKills(guildID, charID uint32) (int, error)
|
CountGuildKills(guildID, charID uint32) (int, error)
|
||||||
ClearTreasureHunt(charID uint32) error
|
ClearTreasureHunt(charID uint32) error
|
||||||
InsertKillLog(charID uint32, monster int, quantity uint8, timestamp time.Time) error
|
InsertKillLog(charID uint32, monster int, quantity uint8, timestamp time.Time) error
|
||||||
ListInvitedCharacters(guildID uint32) ([]*ScoutedCharacter, error)
|
ListInvites(guildID uint32) ([]*GuildInvite, error)
|
||||||
RolloverDailyRP(guildID uint32, noon time.Time) error
|
RolloverDailyRP(guildID uint32, noon time.Time) error
|
||||||
AddWeeklyBonusUsers(guildID uint32, numUsers uint8) error
|
AddWeeklyBonusUsers(guildID uint32, numUsers uint8) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,8 +308,10 @@ type mockGuildRepo struct {
|
|||||||
removeErr error
|
removeErr error
|
||||||
createAppErr error
|
createAppErr error
|
||||||
getMemberErr error
|
getMemberErr error
|
||||||
hasAppResult bool
|
hasAppResult bool
|
||||||
hasAppErr error
|
hasAppErr error
|
||||||
|
hasInviteResult bool
|
||||||
|
hasInviteErr error
|
||||||
listPostsErr error
|
listPostsErr error
|
||||||
createPostErr error
|
createPostErr error
|
||||||
deletePostErr error
|
deletePostErr error
|
||||||
@@ -317,8 +319,10 @@ type mockGuildRepo struct {
|
|||||||
// State tracking
|
// State tracking
|
||||||
disbandedID uint32
|
disbandedID uint32
|
||||||
removedCharID uint32
|
removedCharID uint32
|
||||||
acceptedCharID uint32
|
acceptedCharID uint32
|
||||||
rejectedCharID uint32
|
rejectedCharID uint32
|
||||||
|
acceptInviteCharID uint32
|
||||||
|
declineInviteCharID uint32
|
||||||
savedGuild *Guild
|
savedGuild *Guild
|
||||||
savedMembers []*GuildMember
|
savedMembers []*GuildMember
|
||||||
createdAppArgs []interface{}
|
createdAppArgs []interface{}
|
||||||
@@ -571,10 +575,19 @@ func (m *mockGuildRepo) CountGuildKills(_, _ uint32) (int, error) {
|
|||||||
// No-op stubs for remaining GuildRepo interface methods.
|
// No-op stubs for remaining GuildRepo interface methods.
|
||||||
func (m *mockGuildRepo) ListAll() ([]*Guild, error) { return nil, nil }
|
func (m *mockGuildRepo) ListAll() ([]*Guild, error) { return nil, nil }
|
||||||
func (m *mockGuildRepo) Create(_ uint32, _ string) (int32, error) { return 0, nil }
|
func (m *mockGuildRepo) Create(_ uint32, _ string) (int32, error) { return 0, nil }
|
||||||
func (m *mockGuildRepo) CreateApplicationWithMail(_, _, _ uint32, _ GuildApplicationType, _, _ uint32, _, _ string) error {
|
func (m *mockGuildRepo) CreateInviteWithMail(_, _, _, _, _ uint32, _, _ string) error { return nil }
|
||||||
return nil
|
func (m *mockGuildRepo) HasInvite(_, _ uint32) (bool, error) {
|
||||||
|
return m.hasInviteResult, m.hasInviteErr
|
||||||
|
}
|
||||||
|
func (m *mockGuildRepo) CancelInvite(_ uint32) error { return nil }
|
||||||
|
func (m *mockGuildRepo) AcceptInvite(_, charID uint32) error {
|
||||||
|
m.acceptInviteCharID = charID
|
||||||
|
return m.acceptErr
|
||||||
|
}
|
||||||
|
func (m *mockGuildRepo) DeclineInvite(_, charID uint32) error {
|
||||||
|
m.declineInviteCharID = charID
|
||||||
|
return m.rejectErr
|
||||||
}
|
}
|
||||||
func (m *mockGuildRepo) CancelInvitation(_, _ uint32) error { return nil }
|
|
||||||
func (m *mockGuildRepo) ArrangeCharacters(_ []uint32) error { return nil }
|
func (m *mockGuildRepo) ArrangeCharacters(_ []uint32) error { return nil }
|
||||||
func (m *mockGuildRepo) GetItemBox(_ uint32) ([]byte, error) { return nil, nil }
|
func (m *mockGuildRepo) GetItemBox(_ uint32) ([]byte, error) { return nil, nil }
|
||||||
func (m *mockGuildRepo) SaveItemBox(_ uint32, _ []byte) error { return nil }
|
func (m *mockGuildRepo) SaveItemBox(_ uint32, _ []byte) error { return nil }
|
||||||
@@ -597,9 +610,7 @@ func (m *mockGuildRepo) CountNewPosts(_ uint32, _ time.Time) (int, error)
|
|||||||
func (m *mockGuildRepo) ListAlliances() ([]*GuildAlliance, error) { return nil, nil }
|
func (m *mockGuildRepo) ListAlliances() ([]*GuildAlliance, error) { return nil, nil }
|
||||||
func (m *mockGuildRepo) ClearTreasureHunt(_ uint32) error { return nil }
|
func (m *mockGuildRepo) ClearTreasureHunt(_ uint32) error { return nil }
|
||||||
func (m *mockGuildRepo) InsertKillLog(_ uint32, _ int, _ uint8, _ time.Time) error { return nil }
|
func (m *mockGuildRepo) InsertKillLog(_ uint32, _ int, _ uint8, _ time.Time) error { return nil }
|
||||||
func (m *mockGuildRepo) ListInvitedCharacters(_ uint32) ([]*ScoutedCharacter, error) {
|
func (m *mockGuildRepo) ListInvites(_ uint32) ([]*GuildInvite, error) { return nil, nil }
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
func (m *mockGuildRepo) RolloverDailyRP(_ uint32, _ time.Time) error { return nil }
|
func (m *mockGuildRepo) RolloverDailyRP(_ uint32, _ time.Time) error { return nil }
|
||||||
func (m *mockGuildRepo) AddWeeklyBonusUsers(_ uint32, _ uint8) error { return nil }
|
func (m *mockGuildRepo) AddWeeklyBonusUsers(_ uint32, _ uint8) error { return nil }
|
||||||
|
|
||||||
|
|||||||
@@ -280,16 +280,16 @@ func (svc *GuildService) PostScout(actorCharID, targetCharID uint32, strings Sco
|
|||||||
return fmt.Errorf("guild lookup: %w", err)
|
return fmt.Errorf("guild lookup: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasApp, err := svc.guildRepo.HasApplication(guild.ID, targetCharID)
|
hasInvite, err := svc.guildRepo.HasInvite(guild.ID, targetCharID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("check application: %w", err)
|
return fmt.Errorf("check invite: %w", err)
|
||||||
}
|
}
|
||||||
if hasApp {
|
if hasInvite {
|
||||||
return ErrAlreadyInvited
|
return ErrAlreadyInvited
|
||||||
}
|
}
|
||||||
|
|
||||||
err = svc.guildRepo.CreateApplicationWithMail(
|
err = svc.guildRepo.CreateInviteWithMail(
|
||||||
guild.ID, targetCharID, actorCharID, GuildApplicationTypeInvited,
|
guild.ID, targetCharID, actorCharID,
|
||||||
actorCharID, targetCharID,
|
actorCharID, targetCharID,
|
||||||
strings.Title,
|
strings.Title,
|
||||||
fmt.Sprintf(strings.Body, guild.Name))
|
fmt.Sprintf(strings.Body, guild.Name))
|
||||||
@@ -309,8 +309,8 @@ func (svc *GuildService) AnswerScout(charID, leaderID uint32, accept bool, strin
|
|||||||
return nil, fmt.Errorf("guild lookup for leader %d: %w", leaderID, err)
|
return nil, fmt.Errorf("guild lookup for leader %d: %w", leaderID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app, err := svc.guildRepo.GetApplication(guild.ID, charID, GuildApplicationTypeInvited)
|
hasInvite, err := svc.guildRepo.HasInvite(guild.ID, charID)
|
||||||
if app == nil || err != nil {
|
if err != nil || !hasInvite {
|
||||||
return &AnswerScoutResult{
|
return &AnswerScoutResult{
|
||||||
GuildID: guild.ID,
|
GuildID: guild.ID,
|
||||||
Success: false,
|
Success: false,
|
||||||
@@ -319,13 +319,13 @@ func (svc *GuildService) AnswerScout(charID, leaderID uint32, accept bool, strin
|
|||||||
|
|
||||||
var mails []Mail
|
var mails []Mail
|
||||||
if accept {
|
if accept {
|
||||||
err = svc.guildRepo.AcceptApplication(guild.ID, charID)
|
err = svc.guildRepo.AcceptInvite(guild.ID, charID)
|
||||||
mails = []Mail{
|
mails = []Mail{
|
||||||
{SenderID: 0, RecipientID: charID, Subject: strings.SuccessTitle, Body: fmt.Sprintf(strings.SuccessBody, guild.Name), IsSystemMessage: true},
|
{SenderID: 0, RecipientID: charID, Subject: strings.SuccessTitle, Body: fmt.Sprintf(strings.SuccessBody, guild.Name), IsSystemMessage: true},
|
||||||
{SenderID: charID, RecipientID: leaderID, Subject: strings.AcceptedTitle, Body: fmt.Sprintf(strings.AcceptedBody, guild.Name), IsSystemMessage: true},
|
{SenderID: charID, RecipientID: leaderID, Subject: strings.AcceptedTitle, Body: fmt.Sprintf(strings.AcceptedBody, guild.Name), IsSystemMessage: true},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = svc.guildRepo.RejectApplication(guild.ID, charID)
|
err = svc.guildRepo.DeclineInvite(guild.ID, charID)
|
||||||
mails = []Mail{
|
mails = []Mail{
|
||||||
{SenderID: 0, RecipientID: charID, Subject: strings.RejectedTitle, Body: fmt.Sprintf(strings.RejectedBody, guild.Name), IsSystemMessage: true},
|
{SenderID: 0, RecipientID: charID, Subject: strings.RejectedTitle, Body: fmt.Sprintf(strings.RejectedBody, guild.Name), IsSystemMessage: true},
|
||||||
{SenderID: charID, RecipientID: leaderID, Subject: strings.DeclinedTitle, Body: fmt.Sprintf(strings.DeclinedBody, guild.Name), IsSystemMessage: true},
|
{SenderID: charID, RecipientID: leaderID, Subject: strings.DeclinedTitle, Body: fmt.Sprintf(strings.DeclinedBody, guild.Name), IsSystemMessage: true},
|
||||||
|
|||||||
@@ -385,14 +385,14 @@ func TestGuildService_PostScout(t *testing.T) {
|
|||||||
strings := ScoutInviteStrings{Title: "Invite", Body: "Join 「%s」"}
|
strings := ScoutInviteStrings{Title: "Invite", Body: "Join 「%s」"}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
membership *GuildMember
|
membership *GuildMember
|
||||||
guild *Guild
|
guild *Guild
|
||||||
hasApp bool
|
hasInvite bool
|
||||||
hasAppErr error
|
hasInviteErr error
|
||||||
createAppErr error
|
createAppErr error
|
||||||
getMemberErr error
|
getMemberErr error
|
||||||
wantErr error
|
wantErr error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "successful scout",
|
name: "successful scout",
|
||||||
@@ -403,7 +403,7 @@ func TestGuildService_PostScout(t *testing.T) {
|
|||||||
name: "already invited",
|
name: "already invited",
|
||||||
membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
|
membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
|
||||||
guild: &Guild{ID: 10, Name: "TestGuild"},
|
guild: &Guild{ID: 10, Name: "TestGuild"},
|
||||||
hasApp: true,
|
hasInvite: true,
|
||||||
wantErr: ErrAlreadyInvited,
|
wantErr: ErrAlreadyInvited,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -423,11 +423,11 @@ func TestGuildService_PostScout(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
guildMock := &mockGuildRepo{
|
guildMock := &mockGuildRepo{
|
||||||
membership: tt.membership,
|
membership: tt.membership,
|
||||||
hasAppResult: tt.hasApp,
|
hasInviteResult: tt.hasInvite,
|
||||||
hasAppErr: tt.hasAppErr,
|
hasInviteErr: tt.hasInviteErr,
|
||||||
createAppErr: tt.createAppErr,
|
createAppErr: tt.createAppErr,
|
||||||
getMemberErr: tt.getMemberErr,
|
getMemberErr: tt.getMemberErr,
|
||||||
}
|
}
|
||||||
guildMock.guild = tt.guild
|
guildMock.guild = tt.guild
|
||||||
svc := newTestGuildService(guildMock, &mockMailRepo{})
|
svc := newTestGuildService(guildMock, &mockMailRepo{})
|
||||||
@@ -468,7 +468,7 @@ func TestGuildService_AnswerScout(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
accept bool
|
accept bool
|
||||||
guild *Guild
|
guild *Guild
|
||||||
application *GuildApplication
|
hasInvite bool
|
||||||
acceptErr error
|
acceptErr error
|
||||||
rejectErr error
|
rejectErr error
|
||||||
sendErr error
|
sendErr error
|
||||||
@@ -477,13 +477,13 @@ func TestGuildService_AnswerScout(t *testing.T) {
|
|||||||
wantErr error
|
wantErr error
|
||||||
wantMailCount int
|
wantMailCount int
|
||||||
wantAccepted uint32
|
wantAccepted uint32
|
||||||
wantRejected uint32
|
wantDeclined uint32
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "accept invitation",
|
name: "accept invitation",
|
||||||
accept: true,
|
accept: true,
|
||||||
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
||||||
application: &GuildApplication{GuildID: 10, CharID: 1},
|
hasInvite: true,
|
||||||
wantSuccess: true,
|
wantSuccess: true,
|
||||||
wantMailCount: 2,
|
wantMailCount: 2,
|
||||||
wantAccepted: 1,
|
wantAccepted: 1,
|
||||||
@@ -492,16 +492,16 @@ func TestGuildService_AnswerScout(t *testing.T) {
|
|||||||
name: "decline invitation",
|
name: "decline invitation",
|
||||||
accept: false,
|
accept: false,
|
||||||
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
||||||
application: &GuildApplication{GuildID: 10, CharID: 1},
|
hasInvite: true,
|
||||||
wantSuccess: true,
|
wantSuccess: true,
|
||||||
wantMailCount: 2,
|
wantMailCount: 2,
|
||||||
wantRejected: 1,
|
wantDeclined: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "application missing",
|
name: "application missing",
|
||||||
accept: true,
|
accept: true,
|
||||||
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
||||||
application: nil,
|
hasInvite: false,
|
||||||
wantSuccess: false,
|
wantSuccess: false,
|
||||||
wantErr: ErrApplicationMissing,
|
wantErr: ErrApplicationMissing,
|
||||||
},
|
},
|
||||||
@@ -516,7 +516,7 @@ func TestGuildService_AnswerScout(t *testing.T) {
|
|||||||
name: "mail error is best-effort",
|
name: "mail error is best-effort",
|
||||||
accept: true,
|
accept: true,
|
||||||
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
guild: &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
|
||||||
application: &GuildApplication{GuildID: 10, CharID: 1},
|
hasInvite: true,
|
||||||
sendErr: errors.New("mail failed"),
|
sendErr: errors.New("mail failed"),
|
||||||
wantSuccess: true,
|
wantSuccess: true,
|
||||||
wantMailCount: 2,
|
wantMailCount: 2,
|
||||||
@@ -527,9 +527,9 @@ func TestGuildService_AnswerScout(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
guildMock := &mockGuildRepo{
|
guildMock := &mockGuildRepo{
|
||||||
application: tt.application,
|
hasInviteResult: tt.hasInvite,
|
||||||
acceptErr: tt.acceptErr,
|
acceptErr: tt.acceptErr,
|
||||||
rejectErr: tt.rejectErr,
|
rejectErr: tt.rejectErr,
|
||||||
}
|
}
|
||||||
guildMock.guild = tt.guild
|
guildMock.guild = tt.guild
|
||||||
guildMock.getErr = tt.getErr
|
guildMock.getErr = tt.getErr
|
||||||
@@ -559,11 +559,11 @@ func TestGuildService_AnswerScout(t *testing.T) {
|
|||||||
if len(mailMock.sentMails) != tt.wantMailCount {
|
if len(mailMock.sentMails) != tt.wantMailCount {
|
||||||
t.Errorf("sentMails count = %d, want %d", len(mailMock.sentMails), tt.wantMailCount)
|
t.Errorf("sentMails count = %d, want %d", len(mailMock.sentMails), tt.wantMailCount)
|
||||||
}
|
}
|
||||||
if tt.wantAccepted != 0 && guildMock.acceptedCharID != tt.wantAccepted {
|
if tt.wantAccepted != 0 && guildMock.acceptInviteCharID != tt.wantAccepted {
|
||||||
t.Errorf("acceptedCharID = %d, want %d", guildMock.acceptedCharID, tt.wantAccepted)
|
t.Errorf("acceptInviteCharID = %d, want %d", guildMock.acceptInviteCharID, tt.wantAccepted)
|
||||||
}
|
}
|
||||||
if tt.wantRejected != 0 && guildMock.rejectedCharID != tt.wantRejected {
|
if tt.wantDeclined != 0 && guildMock.declineInviteCharID != tt.wantDeclined {
|
||||||
t.Errorf("rejectedCharID = %d, want %d", guildMock.rejectedCharID, tt.wantRejected)
|
t.Errorf("declineInviteCharID = %d, want %d", guildMock.declineInviteCharID, tt.wantDeclined)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
23
server/migrations/sql/0012_guild_invites.sql
Normal file
23
server/migrations/sql/0012_guild_invites.sql
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Dedicated table for guild-initiated scout invitations, separate from
|
||||||
|
-- player-initiated applications. This gives each invitation a real serial PK
|
||||||
|
-- so the client's InvitationID field can map to an actual database row
|
||||||
|
-- instead of being aliased to the character ID.
|
||||||
|
CREATE TABLE guild_invites (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
guild_id integer REFERENCES guilds(id),
|
||||||
|
character_id integer REFERENCES characters(id),
|
||||||
|
actor_id integer REFERENCES characters(id),
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Migrate any existing scout invitations from guild_applications.
|
||||||
|
INSERT INTO guild_invites (guild_id, character_id, actor_id, created_at)
|
||||||
|
SELECT guild_id, character_id, actor_id, COALESCE(created_at, now())
|
||||||
|
FROM guild_applications
|
||||||
|
WHERE application_type = 'invited';
|
||||||
|
|
||||||
|
DELETE FROM guild_applications WHERE application_type = 'invited';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in New Issue
Block a user