Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt to use the .env instead.

Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ Thumbs.db
.idea/
*.swp
*.swo
client_secret.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc;
using TRFSAE.MemberPortal.API.DTOs;
using TRFSAE.MemberPortal.API.Interfaces;
using Supabase;

namespace TRFSAE.MemberPortal.API.Controllers
{
[ApiController]
[Route("api/purchase-request")]
public class PurchaseRequestController : ControllerBase
{
private readonly IPurchaseRequestService _purchaseRequestService;

public PurchaseRequestController(IPurchaseRequestService purchaseRequestService)
{
_purchaseRequestService = purchaseRequestService;
}

[HttpGet]
public async Task<IActionResult> GetAllPurchaseAsync([FromQuery] PurchaseRequestSearchDto? dto)
{
var result = await _purchaseRequestService.GetAllPurchaseRequestsAsync(dto ?? new PurchaseRequestSearchDto());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we making a new DTO here? There should only exist 1 instance of a DTO as it's a singleton.

return Ok(result);
}

[HttpGet("{id:guid}", Name = "GetPurchaseRequestById")]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use {id} instead.

public async Task<IActionResult> GetPurchaseRequestByIDAsync(Guid id)
{
var item = await _purchaseRequestService.GetPurchaseRequestByIDAsync(id);
return item is null ? NotFound() : Ok(item);
}

[HttpPost]
public async Task<IActionResult> CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto)
{
var created = await _purchaseRequestService.CreatePurchaseRequestAsync(dto);
return CreatedAtRoute("GetPurchaseRequestById", new { id = created.Id }, created);
}

[HttpPut("{id:guid}")]
public async Task<IActionResult> UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto)
{
var updated = await _purchaseRequestService.UpdatePurchaseRequestByIDAsync(id, dto);
return updated is null ? NotFound() : Ok(updated);
}

[HttpDelete("{id:guid}")]
public async Task<IActionResult> DeletePurchaseRequestAsync(Guid id, string confirmationString)
{
var deleted = await _purchaseRequestService.DeletePurchaseRequestAsync(id, confirmationString);
return deleted ? NoContent() : NotFound();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TRFSAE.MemberPortal.API.DTOs
{
public class PurchaseRequestCreateDto
{
public string Status { get; set; } = string.Empty;
public Guid Requester { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;

namespace TRFSAE.MemberPortal.API.DTOs
{
public class PurchaseRequestResponseDto
{
[JsonPropertyName("id")]
public Guid Id { get; set; }

[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;

[JsonPropertyName("requester")]
public Guid? Requester { get; set; }

[JsonPropertyName("created_at")]
public DateTimeOffset CreatedAt { get; set; }

[JsonPropertyName("updated_at")]
public DateTime UpdatedAt { get; set; }
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to be able to search by informal ID (i.e. TR26-001), author, and subsystem.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TRFSAE.MemberPortal.API.DTOs
{
public class PurchaseRequestSearchDto
{
public string Status { get; set; } = string.Empty;
public Guid? Requester { get; set; }
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incomplete.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TRFSAE.MemberPortal.API.DTOs
{
public class PurchaseRequestUpdateDto
{
public string Status { get; set; } = string.Empty;
public Guid Requester { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using TRFSAE.MemberPortal.API.DTOs;

namespace TRFSAE.MemberPortal.API.Interfaces
{
public interface IPurchaseRequestService
{
Task<List<PurchaseRequestResponseDto>> GetAllPurchaseRequestsAsync(PurchaseRequestSearchDto dto);
Task<PurchaseRequestResponseDto> GetPurchaseRequestByIDAsync(Guid id);
Task<PurchaseRequestResponseDto> CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto);
Task<PurchaseRequestResponseDto> UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto);
Task<bool> DeletePurchaseRequestAsync(Guid id, string confirmationString);
}
}
24 changes: 24 additions & 0 deletions backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incomplete.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models;

namespace TRFSAE.MemberPortal.API.Models
{
[Table("purchase_request")]
public class PurchaseRequestModel: BaseModel
{
[PrimaryKey("id", false)]
public Guid Id { get; set; }

[Column("status")]
public string Status { get; set; } = string.Empty;

[Column("requester")]
public Guid Requester { get; set; }

[Column("created_at")]
public DateTimeOffset CreatedAt { get; set; }

[Column("updated_at")]
public DateTime UpdatedAt { get; set; }
}
}
38 changes: 18 additions & 20 deletions backend/TRFSAE.MemberPortal.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
builder.Services.AddScoped<IRoleService, RoleService>();
builder.Services.AddScoped<ITaskService, TaskService>();
builder.Services.AddScoped<IProjectService, ProjectService>();
builder.Services.AddScoped<IPurchaseItemService, PurchaseItemService>();
builder.Services.AddScoped<IPurchaseRequestService, PurchaseRequestService>();
builder.Services.AddScoped<IGoogleSheetsService, GoogleSheetsService>();
}

Expand All @@ -35,20 +37,6 @@
return client;
});

// register Supabase client as a singleton for reuse across project
builder.Services.AddScoped(provider =>
{
var options = new SupabaseOptions
{
AutoConnectRealtime = true,
AutoRefreshToken = true,
};

var url = builder.Configuration["SupabaseUrl"] ?? throw new InvalidOperationException("Supabase URL is not configured.");
var key = builder.Configuration["SupabaseKey"] ?? throw new InvalidOperationException("Supabase Key is not configured.");
return new Client(url, key, options);
});

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddEndpointsApiExplorer();
Expand Down Expand Up @@ -81,12 +69,22 @@
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var googleSheetsService = scope.ServiceProvider.GetRequiredService<IGoogleSheetsService>();

// Initialize Google Sheets API once
await googleSheetsService.ListenToSupabaseChangesAsync();
}
// Initialize Google Sheets listener after app is built
app.Lifetime.ApplicationStarted.Register(async () =>
{
using (var scope = app.Services.CreateScope())
{
var googleSheetsService = scope.ServiceProvider.GetRequiredService<IGoogleSheetsService>();
try
{
await googleSheetsService.ListenToSupabaseChangesAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error initializing Google Sheets listener: {ex.Message}");
}
}
});

app.Run();
36 changes: 22 additions & 14 deletions backend/TRFSAE.MemberPortal.API/Services/GoogleSheetsService.cs
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to handle upserts.

Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,30 @@ public async Task<List<SheetsResponseDTO>> GetSupabase()

public async Task ListenToSupabaseChangesAsync()
{
var channel = await _supabaseClient
.From<PurchaseItemModel>()
.On(Supabase.Realtime.PostgresChanges.PostgresChangesOptions.ListenType.Inserts, async (sender, change) =>
{
var items = await GetSupabase();
if (items != null && items.Count > 0)
{
CreateEntry(items);
}
else
try
{
var channel = await _supabaseClient
.From<PurchaseItemModel>()
.On(Supabase.Realtime.PostgresChanges.PostgresChangesOptions.ListenType.Inserts, async (sender, change) =>
{
Console.WriteLine(" No item found to write to Google Sheets");
}
});
var items = await GetSupabase();
if (items != null && items.Count > 0)
{
CreateEntry(items);
}
else
{
Console.WriteLine("No item found to write to Google Sheets");
}
});

await channel.Subscribe();
await channel.Subscribe();
}
catch (Exception ex)
{
Console.WriteLine($"Error in ListenToSupabaseChangesAsync: {ex.Message}");
throw;
}
}

}
Expand Down
140 changes: 140 additions & 0 deletions backend/TRFSAE.MemberPortal.API/Services/PurchaseRequestService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using TRFSAE.MemberPortal.API.DTOs;
using TRFSAE.MemberPortal.API.Interfaces;
using TRFSAE.MemberPortal.API.Models;
using System.Text.Json;
using Supabase;

namespace TRFSAE.MemberPortal.API.Services
{
public class PurchaseRequestService : IPurchaseRequestService
{
private readonly Client _supabaseClient;

public PurchaseRequestService(Client supabaseClient)
{
_supabaseClient = supabaseClient;
}

private PurchaseRequestResponseDto MapToDto(PurchaseRequestModel model)
{
return new PurchaseRequestResponseDto
{
Id = model.Id,
Status = model.Status,
Requester = model.Requester,
CreatedAt = model.CreatedAt,
UpdatedAt = model.UpdatedAt
};
}

private PurchaseRequestModel MapToModel(PurchaseRequestCreateDto dto)
{
return new PurchaseRequestModel
{
Status = dto.Status,
Requester = dto.Requester
};
}

public async Task<List<PurchaseRequestResponseDto>> GetAllPurchaseRequestsAsync(PurchaseRequestSearchDto dto)
{
var response = await _supabaseClient
.From<PurchaseRequestModel>()
.Get();

var results = response.Models.AsEnumerable();

if (!string.IsNullOrWhiteSpace(dto.Status))
{
results = results.Where(x => x.Status == dto.Status);
}

if (dto.Requester.HasValue && dto.Requester != Guid.Empty)
{
results = results.Where(x => x.Requester == dto.Requester);
}

return results.Select(MapToDto).ToList();
}

public async Task<PurchaseRequestResponseDto> GetPurchaseRequestByIDAsync(Guid id)
{
var response = await _supabaseClient
.From<PurchaseRequestModel>()
.Where(x => x.Id == id)
.Single();

if (response == null)
{
throw new Exception("Purchase request not found");
}

return MapToDto(response);
}

public async Task<PurchaseRequestResponseDto> CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto)
{
var newModel = MapToModel(dto);
if (newModel.Id == Guid.Empty)
newModel.Id = Guid.NewGuid();
if (newModel.CreatedAt == default)
newModel.CreatedAt = DateTimeOffset.UtcNow;

var response = await _supabaseClient
.From<PurchaseRequestModel>()
.Insert(new List<PurchaseRequestModel> { newModel });

if (response.Models is null || response.Models.Count == 0)
throw new Exception("Failed to create purchase request");

return MapToDto(response.Models.First());
}

public async Task<PurchaseRequestResponseDto> UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto)
{
var currentRequest = await _supabaseClient
.From<PurchaseRequestModel>()
.Where(x => x.Id == id)
.Single();

if (currentRequest == null)
throw new Exception("Purchase request not found");

if (!string.IsNullOrWhiteSpace(dto.Status))
currentRequest.Status = dto.Status;

if (dto.Requester != Guid.Empty)
currentRequest.Requester = dto.Requester;

currentRequest.UpdatedAt = DateTime.UtcNow;

var response = await _supabaseClient
.From<PurchaseRequestModel>()
.Update(currentRequest);

if (response.Models is null || response.Models.Count == 0)
throw new Exception("Failed to update purchase request");

return MapToDto(response.Models.First());
}

public async Task<bool> DeletePurchaseRequestAsync(Guid id, string confirmationString)
{
if (confirmationString != "confirm")
return false;

try
{
await _supabaseClient
.From<PurchaseRequestModel>()
.Where(x => x.Id == id)
.Delete();
return true;
}
catch
{
return false;
}
}
}
}