const std = @import("std"); const expect = std.testing.expect; const heap = std.heap; const hash_map = std.hash_map; const mem = std.mem; const Thread = std.Thread; const Mutex = Thread.Mutex; const Allocator = mem.Allocator; const testing = std.testing; const DateTime = @import("things").DateTime; const SubscriberFunc = fn (payment: *Payment) void; pub const PaymentIntegrationStatus = enum { processing, processed, not_integrated }; pub const Payment = struct { id: [36]u8, amount: f64, requested_at: DateTime, integration_status: PaymentIntegrationStatus = .processing, processed_by: u64 = undefined, pub fn getIntegrationStatus(self: *const Payment) []const u8 { return switch (self.integration_status) { .processing => "processing", .processed => "processed", .not_integrated => "not integrated" }; } }; pub const PaymentsSummary = struct { total_payments_processed: usize, total_value: f64, }; pub const PaymentsIntegrationSummary = struct { default_total_payments_processed: usize, default_total_value: f64, fallback_total_payments_processed: usize, fallback_total_value: f64, }; const CONTAINER_INCREASE_RATE = 32; const MAX_CONTAINER_INCREASE = 32 * CONTAINER_INCREASE_RATE; inline fn calculateMaxIndexes(tee: usize) usize { return ((tee / CONTAINER_INCREASE_RATE) + 1); } pub inline fn calculateNecessaryMemory(tee: usize) usize { return (@sizeOf(Payment) * tee) + (@sizeOf(IndexContainerUnsafe) * calculateMaxIndexes(tee)) + (calculateMaxIndexes(tee) * MAX_CONTAINER_INCREASE); } inline fn newContainerSize(c: usize) usize { return c + CONTAINER_INCREASE_RATE; } const IndexContainerUnsafe = struct { len: usize = 0, container_size: usize = CONTAINER_INCREASE_RATE, container: []*const Payment = undefined, allocator: Allocator, pub fn init(allocator: Allocator) !IndexContainerUnsafe { const container = try allocator.alloc(*const Payment, CONTAINER_INCREASE_RATE); return IndexContainerUnsafe{ .container = container, .allocator = allocator }; } pub fn add(self: *IndexContainerUnsafe, payment: *const Payment) !void { if (self.len == self.container_size) { const new_container_size = newContainerSize(self.len); self.container = try self.allocator.realloc(self.container, new_container_size); self.container_size = new_container_size; } self.container[self.len] = payment; self.len += 1; } pub fn find(self: *IndexContainerUnsafe, target: []const u8) ?*const Payment { for (0..self.len) |i| { if (mem.eql(u8, target, &self.container[i].id)) return self.container[i]; } return null; } pub fn reset(self: *IndexContainerUnsafe) void { self.len = 0; } pub fn deinit(self: *IndexContainerUnsafe) void { self.len = 0; self.container_size = 0; self.allocator.free(self.container); } }; pub const PaymentsRepository = struct { len: usize = 0, max_indexes: usize = 1, database_size: usize, payments: []Payment, indexes: []IndexContainerUnsafe, mutex: Mutex, allocator: Allocator, subscribers_insert: [2]*const SubscriberFunc = undefined, subscribers_insert_len: usize = 0, pub fn init(allocator: Allocator, database_size: usize) !PaymentsRepository { const payments = try allocator.alloc(Payment, database_size); const max_indexes = calculateMaxIndexes(database_size); var indexes = try allocator.alloc(IndexContainerUnsafe, max_indexes); for (0..indexes.len) |i| { indexes[i] = try IndexContainerUnsafe.init(allocator); } return PaymentsRepository{ .allocator = allocator, .mutex = Mutex{}, .database_size = database_size, .payments = payments, .indexes = indexes, .max_indexes = max_indexes }; } pub fn subscribeInsert(self: *PaymentsRepository, func: *const SubscriberFunc) void { self.subscribers_insert[self.subscribers_insert_len] = func; self.subscribers_insert_len += 1; } pub fn insert(self: *PaymentsRepository, payment: Payment) !void { self.mutex.lock(); defer self.mutex.unlock(); if (self.len == self.database_size) return Allocator.Error.OutOfMemory; self.payments[self.len] = payment; const id_hash = self.hash(&payment.id); try self.indexes[id_hash].add(&self.payments[self.len]); for (self.subscribers_insert[0..self.subscribers_insert_len]) |func| { func(&self.payments[self.len]); } self.len += 1; } pub fn findById(self: *PaymentsRepository, id: []const u8) ?*const Payment { self.mutex.lock(); defer self.mutex.unlock(); if (self.len == 0) return null; const id_hash = self.hash(id); const ic = self.indexes[id_hash].find(id); return ic; } pub fn reset(self: *PaymentsRepository) void { self.mutex.lock(); defer self.mutex.unlock(); self.len = 0; for (0..self.indexes.len) |i| { self.indexes[i].reset(); } } pub fn periodSummary(self: *PaymentsRepository, from: ?DateTime, to: ?DateTime) PaymentsSummary { self.mutex.lock(); const len = self.len; self.mutex.unlock(); var payments_summary = PaymentsSummary{ .total_payments_processed = 0, .total_value = 0 }; if (from != null and to != null) { for (0..len) |i| { const payment = self.payments[i]; if (payment.requested_at.gt(from.?) and payment.requested_at.lt(to.?)) { payments_summary.total_payments_processed += 1; payments_summary.total_value += payment.amount; } } } else if (from != null) { for (0..len) |i| { const payment = self.payments[i]; if (payment.requested_at.gt(from.?)) { payments_summary.total_payments_processed += 1; payments_summary.total_value += payment.amount; } } } else if (to != null) { for (0..len) |i| { const payment = self.payments[i]; if (payment.requested_at.lt(to.?)) { payments_summary.total_payments_processed += 1; payments_summary.total_value += payment.amount; } } } else { for (0..len) |i| { payments_summary.total_value += self.payments[i].amount; } payments_summary.total_payments_processed = len; } return payments_summary; } pub fn integrationSummary(self: *PaymentsRepository, from: ?DateTime, to: ?DateTime) PaymentsIntegrationSummary { self.mutex.lock(); const len = self.len; self.mutex.unlock(); var summary = PaymentsIntegrationSummary{ .default_total_payments_processed = 0, .default_total_value = 0, .fallback_total_payments_processed = 0, .fallback_total_value = 0, }; if (from != null and to != null) { for (0..len) |i| { const payment = self.payments[i]; if (payment.requested_at.gt(from.?) and payment.requested_at.lt(to.?)) { if (payment.integration_status != .processed) continue; if (payment.processed_by == 1) { summary.default_total_payments_processed += 1; summary.default_total_value += payment.amount; } else { summary.fallback_total_payments_processed += 1; summary.fallback_total_value += payment.amount; } } } } else if (from != null) { for (0..len) |i| { const payment = self.payments[i]; if (payment.requested_at.gt(from.?)) { if (payment.integration_status != .processed) continue; if (payment.processed_by == 1) { summary.default_total_payments_processed += 1; summary.default_total_value += payment.amount; } else { summary.fallback_total_payments_processed += 1; summary.fallback_total_value += payment.amount; } } } } else if (to != null) { for (0..len) |i| { const payment = self.payments[i]; if (payment.requested_at.lt(to.?)) { if (payment.integration_status != .processed) continue; if (payment.processed_by == 1) { summary.default_total_payments_processed += 1; summary.default_total_value += payment.amount; } else { summary.fallback_total_payments_processed += 1; summary.fallback_total_value += payment.amount; } } } } else { for (0..len) |i| { const payment = self.payments[i]; if (payment.integration_status != .processed) continue; if (payment.processed_by == 1) { summary.default_total_payments_processed += 1; summary.default_total_value += payment.amount; } else { summary.fallback_total_payments_processed += 1; summary.fallback_total_value += payment.amount; } } } return summary; } fn hash(self: *PaymentsRepository, s: []const u8) usize { std.debug.assert(self.max_indexes > 0); return hash_map.hashString(s) % self.max_indexes; } pub fn deinit(self: *PaymentsRepository) void { self.mutex.lock(); defer self.mutex.unlock(); for (self.indexes) |ic| { @constCast(&ic).deinit(); } self.allocator.free(self.indexes); self.len = 0; self.database_size = 0; self.max_indexes = 0; self.allocator.free(self.payments); } }; fn fakeGuid(end: u32) [36]u8 { var guid: [36]u8 = .{0} ** 36; mem.writeInt(u32, guid[0..4], end, .little); return guid; } test "expect add payment to index container" { var index_container = try IndexContainerUnsafe.init(testing.allocator); defer index_container.deinit(); const payment = Payment{ .id = fakeGuid(1), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }; try index_container.add(&payment); try index_container.add(&payment); try index_container.add(&payment); try index_container.add(&payment); try expect(index_container.len == 4); } test "expect find inserted payment" { var index_container = try IndexContainerUnsafe.init(testing.allocator); defer index_container.deinit(); try index_container.add(&Payment{ .id = fakeGuid(1), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }); try index_container.add(&Payment{ .id = fakeGuid(2), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }); try index_container.add(&Payment{ .id = fakeGuid(3), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }); try index_container.add(&Payment{ .id = fakeGuid(4), .amount = 23.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }); try index_container.add(&Payment{ .id = fakeGuid(5), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }); const payment_target = index_container.find(&fakeGuid(4)); try expect(payment_target.?.amount == 23.0); } test "expect return null payment not exists" { var index_container = try IndexContainerUnsafe.init(testing.allocator); defer index_container.deinit(); try index_container.add(&Payment{ .id = fakeGuid(1), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }); const payment_target = index_container.find(&fakeGuid(4)); try expect(payment_target == null); } test "expect allocate memory when container is full" { const buffer: []u8 = try testing.allocator.alloc(u8, @sizeOf(*void) * 1000); defer testing.allocator.free(buffer); var fba = heap.FixedBufferAllocator.init(buffer); const allocator = fba.allocator(); var index_container = try IndexContainerUnsafe.init(allocator); defer index_container.deinit(); for (0..923) |i| { try (&index_container).add(&Payment{ .id = fakeGuid(@intCast(i)), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }); } try expect(index_container.container_size == ((923 / CONTAINER_INCREASE_RATE) + 1) * CONTAINER_INCREASE_RATE); } test "expect OutOfMemory error when allocator not has memory" { const buffer: []u8 = try testing.allocator.alloc(u8, @sizeOf(*void) * 109); defer testing.allocator.free(buffer); var fba = heap.FixedBufferAllocator.init(buffer); const allocator = fba.allocator(); var index_container = try IndexContainerUnsafe.init(allocator); defer index_container.deinit(); for (0..101) |i| { (&index_container).add(&Payment{ .id = fakeGuid(@intCast(i)), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }) catch |err| { try expect(err == Allocator.Error.OutOfMemory); return; }; } try expect(false); } test "expect insert payment into repository" { var repository = try PaymentsRepository.init(testing.allocator, 100); defer repository.deinit(); const payment1 = Payment{ .id = fakeGuid(1), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }; const payment2 = Payment{ .id = fakeGuid(2), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }; try repository.insert(payment1); try repository.insert(payment2); try expect(repository.len == 2); } test "expect OutOfMemory error when database is full" { var repository = try PaymentsRepository.init(testing.allocator, 100); defer (&repository).deinit(); for (0..101) |i| { const payment = Payment{ .id = fakeGuid(@intCast(i)), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }; (&repository).insert(payment) catch |err| { try expect(err == Allocator.Error.OutOfMemory); return; }; } try expect(false); } test "expect return payment if exists" { var repository: *PaymentsRepository = @constCast(&try PaymentsRepository.init(testing.allocator, 100)); defer repository.deinit(); try repository.insert(Payment{ .id = fakeGuid(1), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(2), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(3), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(23), .amount = 42.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(4), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(7), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(10), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); const payment_result = repository.findById(&fakeGuid(23)); try expect(payment_result.?.amount == 42); } test "expect return null if payment not exists" { var repository: *PaymentsRepository = @constCast(&try PaymentsRepository.init(testing.allocator, 100)); defer repository.deinit(); try repository.insert(Payment{ .id = fakeGuid(1), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(2), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(3), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(23), .amount = 42.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(4), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(7), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(10), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); const payment_result = repository.findById(&fakeGuid(42)); try expect(payment_result == null); } fn testConcurrenceInsert(repository: *PaymentsRepository, interations: usize) !void { for (0..interations) |i| { try repository.insert(Payment{ .id = fakeGuid(@intCast(i)), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2011-10-05T14:48:00.000Z") }); } } fn testConcurrenceFind(repository: *PaymentsRepository, interations: usize) void { for (0..interations) |i| { _ = repository.findById(&fakeGuid(@intCast(i))); } } test "expect avoid concurrency" { const interations = 100; const buffer: []u8 = try testing.allocator.alloc(u8, calculateNecessaryMemory(interations)); defer testing.allocator.free(buffer); var fba = heap.FixedBufferAllocator.init(buffer); const allocator = fba.allocator(); var repository: *PaymentsRepository = @constCast(&try PaymentsRepository.init(allocator, interations)); defer repository.deinit(); const threadInsert1 = try Thread.spawn(.{}, testConcurrenceInsert, .{ repository, interations / 2 }); const threadInsert2 = try Thread.spawn(.{}, testConcurrenceInsert, .{ repository, interations / 2 }); const threadFind = try Thread.spawn(.{}, testConcurrenceFind, .{ repository, interations }); threadInsert1.join(); threadInsert2.join(); threadFind.join(); try expect(repository.len == interations); } test "expect reset repository and indexes" { var repository: *PaymentsRepository = @constCast(&try PaymentsRepository.init(testing.allocator, 100)); defer repository.deinit(); try repository.insert(Payment{ .id = fakeGuid(1), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(2), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(3), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(23), .amount = 42.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(4), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(7), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(10), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); repository.reset(); try expect(repository.len == 0); for (repository.indexes) |ic| { try expect(ic.len == 0); } } test "expect summary return correct value" { var repository = try PaymentsRepository.init(testing.allocator, 100); defer repository.deinit(); try repository.insert(Payment{ .id = fakeGuid(1), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(2), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(3), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(23), .amount = 42.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(4), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(7), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(10), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.000Z") }); const summary = repository.periodSummary(null, null); try expect(summary.total_payments_processed == 7); try expect(summary.total_value == 102.0); } test "expect summary return correct value with filter from" { var repository = try PaymentsRepository.init(testing.allocator, 100); defer repository.deinit(); const from = try DateTime.ParseFromIso("2025-10-05T14:48:00.030Z"); try repository.insert(Payment{ .id = fakeGuid(1), .amount = 42.0, .requested_at = try DateTime.ParseFromIso("2024-10-05T13:47:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(2), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2024-10-05T14:48:00.033Z") }); try repository.insert(Payment{ .id = fakeGuid(3), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-09-05T14:48:00.100Z") }); try repository.insert(Payment{ .id = fakeGuid(4), .amount = 23.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.030Z") }); try repository.insert(Payment{ .id = fakeGuid(7), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.100Z") }); try repository.insert(Payment{ .id = fakeGuid(6), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:50:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(5), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-11-05T14:48:00.030Z") }); const summary = repository.periodSummary(from, null); try expect(summary.total_payments_processed == 4); try expect(summary.total_value == 53.0); } test "expect summary return correct correct value with filter to" { var repository = try PaymentsRepository.init(testing.allocator, 100); defer repository.deinit(); const to = try DateTime.ParseFromIso("2025-10-05T14:48:00.030Z"); try repository.insert(Payment{ .id = fakeGuid(1), .amount = 42.0, .requested_at = try DateTime.ParseFromIso("2024-10-05T13:47:00.000Z") }); // x try repository.insert(Payment{ .id = fakeGuid(2), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2024-10-05T14:48:00.033Z") }); // x try repository.insert(Payment{ .id = fakeGuid(3), .amount = 11.0, .requested_at = try DateTime.ParseFromIso("2025-09-05T14:48:00.100Z") }); // x try repository.insert(Payment{ .id = fakeGuid(4), .amount = 23.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.030Z") }); // x // try repository.insert(Payment{ .id = fakeGuid(6), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:50:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(7), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.100Z") }); try repository.insert(Payment{ .id = fakeGuid(5), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-11-05T14:48:00.030Z") }); const summary = repository.periodSummary(null, to); try expect(summary.total_payments_processed == 4); try expect(summary.total_value == 86.0); } test "expect summary return correct value with filter from and to" { var repository = try PaymentsRepository.init(testing.allocator, 100); defer repository.deinit(); const from = try DateTime.ParseFromIso("2025-10-05T14:48:00.030Z"); const to = try DateTime.ParseFromIso("2025-10-05T14:48:00.100Z"); try repository.insert(Payment{ .id = fakeGuid(1), .amount = 42.0, .requested_at = try DateTime.ParseFromIso("2024-10-05T13:47:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(2), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2024-10-05T14:48:00.033Z") }); try repository.insert(Payment{ .id = fakeGuid(3), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-09-05T14:48:00.100Z") }); try repository.insert(Payment{ .id = fakeGuid(4), .amount = 23.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.030Z") }); // x try repository.insert(Payment{ .id = fakeGuid(5), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-11-05T14:48:00.030Z") }); try repository.insert(Payment{ .id = fakeGuid(6), .amount = 10.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:50:00.000Z") }); try repository.insert(Payment{ .id = fakeGuid(7), .amount = 42.0, .requested_at = try DateTime.ParseFromIso("2025-10-05T14:48:00.100Z") }); // x const summary = repository.periodSummary(from, to); try expect(summary.total_payments_processed == 2); try expect(summary.total_value == 65.0); }