diff --git a/CI.xctestplan b/CI.xctestplan index 1bdf5da1..d3e9b3f6 100644 --- a/CI.xctestplan +++ b/CI.xctestplan @@ -21,7 +21,8 @@ "referenceType" : "built-in" }, "region" : "JP", - "testTimeoutsEnabled" : true + "testTimeoutsEnabled" : true, + "timeZone" : "Asia\/Tokyo" }, "testTargets" : [ { diff --git a/homete/Model/Dependencies/HouseworkClient.swift b/homete/Model/Dependencies/HouseworkClient.swift index 12384a1e..af3f77d1 100644 --- a/homete/Model/Dependencies/HouseworkClient.swift +++ b/homete/Model/Dependencies/HouseworkClient.swift @@ -64,11 +64,10 @@ extension HouseworkClient: DependencyClient { } } snapshotListener: { id, cohabitantId, anchorDate, offset in - let targetDateList = calcTargetPeriod( + let targetDateList = HouseworkIndexedDate.calcTargetPeriod( anchorDate: anchorDate, offsetDays: offset, - calendar: Calendar.autoupdatingCurrent, - locale: .current + calendar: .autoupdatingCurrent ) return await FirestoreService.shared.addSnapshotListener(id: id) { @@ -83,26 +82,3 @@ extension HouseworkClient: DependencyClient { static let previewValue = HouseworkClient() } - -private extension HouseworkClient { - - static func calcTargetPeriod( - anchorDate: Date, - offsetDays: Int, - calendar: Calendar, - locale: Locale - ) -> [[String: String]] { - - let base = calendar.startOfDay(for: anchorDate) - guard offsetDays >= 0 else { - - return [["value": HouseworkIndexedDate(base, locale: locale).value]] - } - // -offset ... +offset の範囲を列挙 - return (-offsetDays...offsetDays).compactMap { delta in - - guard let date = calendar.date(byAdding: .day, value: delta, to: base) else { return nil } - return ["value": HouseworkIndexedDate(base, locale: locale).value] - } - } -} diff --git a/homete/Model/Domain/Cohabitant/Housework/DailyHouseworkList.swift b/homete/Model/Domain/Cohabitant/Housework/DailyHouseworkList.swift index 419cb7b5..c4a5d194 100644 --- a/homete/Model/Domain/Cohabitant/Housework/DailyHouseworkList.swift +++ b/homete/Model/Domain/Cohabitant/Housework/DailyHouseworkList.swift @@ -15,13 +15,12 @@ struct DailyHouseworkList: Equatable, Sendable { static func makeInitialValue( selectedDate: Date, items: [HouseworkItem], - calendar: Calendar, - locale: Locale + calendar: Calendar ) -> Self { return .init( items: items, - metaData: .init(selectedDate: selectedDate, calendar: calendar, locale: locale) + metaData: .init(selectedDate: selectedDate, calendar: calendar) ) } diff --git a/homete/Model/Domain/Cohabitant/Housework/DailyHouseworkMetaData.swift b/homete/Model/Domain/Cohabitant/Housework/DailyHouseworkMetaData.swift index 6d081d5f..54d59c20 100644 --- a/homete/Model/Domain/Cohabitant/Housework/DailyHouseworkMetaData.swift +++ b/homete/Model/Domain/Cohabitant/Housework/DailyHouseworkMetaData.swift @@ -15,9 +15,9 @@ struct DailyHouseworkMetaData: Equatable { extension DailyHouseworkMetaData { - init(selectedDate: Date, calendar: Calendar, locale: Locale) { + init(selectedDate: Date, calendar: Calendar) { - let indexedDate = HouseworkIndexedDate(selectedDate, locale: locale) + let indexedDate = HouseworkIndexedDate(selectedDate, calendar: calendar) let expiredAt = calendar.date(byAdding: .month, value: 1, to: selectedDate) ?? selectedDate self.init(indexedDate: indexedDate, expiredAt: expiredAt) } diff --git a/homete/Model/Domain/Cohabitant/Housework/HouseworkBoardList.swift b/homete/Model/Domain/Cohabitant/Housework/HouseworkBoardList.swift index 0dcbcf34..abbf5571 100644 --- a/homete/Model/Domain/Cohabitant/Housework/HouseworkBoardList.swift +++ b/homete/Model/Domain/Cohabitant/Housework/HouseworkBoardList.swift @@ -22,12 +22,12 @@ extension HouseworkBoardList { init( dailyList: [DailyHouseworkList], selectedDate: Date, - locale: Locale + calendar: Calendar ) { items = dailyList .first { - $0.metaData.indexedDate == .init(selectedDate, locale: locale) + $0.metaData.indexedDate == .init(selectedDate, calendar: calendar) }?.items ?? [] } } diff --git a/homete/Model/Domain/Cohabitant/Housework/HouseworkIndexedDate.swift b/homete/Model/Domain/Cohabitant/Housework/HouseworkIndexedDate.swift index d2f66ad0..89756700 100644 --- a/homete/Model/Domain/Cohabitant/Housework/HouseworkIndexedDate.swift +++ b/homete/Model/Domain/Cohabitant/Housework/HouseworkIndexedDate.swift @@ -8,20 +8,42 @@ import Foundation struct HouseworkIndexedDate: Equatable, Codable, Hashable { + let value: String + + static func calcTargetPeriod( + anchorDate: Date, + offsetDays: Int, + calendar: Calendar + ) -> [[String: String]] { + + let base = calendar.startOfDay(for: anchorDate) + guard offsetDays >= 0 else { + + return [["value": HouseworkIndexedDate(base, calendar: calendar).value]] + } + // -offset ... +offset の範囲を列挙 + return (-offsetDays...offsetDays).compactMap { delta in + + guard let date = calendar.date(byAdding: .day, value: delta, to: base) else { return nil } + return ["value": HouseworkIndexedDate(date, calendar: calendar).value] + } + } } extension HouseworkIndexedDate { - private static let formatStyle = Date.FormatStyle(date: .numeric, time: .omitted) - .year(.extended(minimumLength: 4)) - .month(.twoDigits) - .day(.twoDigits) - - init(_ date: Date, locale: Locale = .init(identifier: "ja_JP")) { - value = date.formatted( - Self.formatStyle - .locale(locale) + init(_ date: Date, calendar: Calendar) { + let formatStyle = Date.FormatStyle( + date: .numeric, + time: .omitted, + locale: calendar.locale ?? .autoupdatingCurrent, + calendar: calendar, + timeZone: calendar.timeZone ) + .year(.extended(minimumLength: 4)) + .month(.twoDigits) + .day(.twoDigits) + value = date.formatted(formatStyle) } } diff --git a/homete/Model/Domain/Cohabitant/Housework/HouseworkItem.swift b/homete/Model/Domain/Cohabitant/Housework/HouseworkItem.swift index e87c6445..76dc04a5 100644 --- a/homete/Model/Domain/Cohabitant/Housework/HouseworkItem.swift +++ b/homete/Model/Domain/Cohabitant/Housework/HouseworkItem.swift @@ -10,12 +10,25 @@ import Foundation struct HouseworkItem: Identifiable, Equatable, Sendable, Hashable, Codable { let id: String + /// 家事の日付情報 let indexedDate: HouseworkIndexedDate + /// 家事のタイトル let title: String + /// 家事ポイント let point: Int + /// 家事ステータス let state: HouseworkState + /// 実行者のユーザID let executorId: String? + /// 実行日時 let executedAt: Date? + /// 確認者のユーザID + let reviewerId: String? + /// 承認日時 + let approvedAt: Date? + /// 確認コメント + let reviewerComment: String? + /// 有効期限 let expiredAt: Date var formattedIndexedDate: String { @@ -23,6 +36,12 @@ struct HouseworkItem: Identifiable, Equatable, Sendable, Hashable, Codable { return indexedDate.value } + /// レビュー可能かどうか + func canReview(ownUserId: String) -> Bool { + + return executorId != ownUserId && state != .completed + } + func updatePendingApproval(at now: Date, changer: String) -> Self { return .init( @@ -33,6 +52,26 @@ struct HouseworkItem: Identifiable, Equatable, Sendable, Hashable, Codable { state: .pendingApproval, executorId: changer, executedAt: now, + reviewerId: reviewerId, + approvedAt: approvedAt, + reviewerComment: reviewerComment, + expiredAt: expiredAt + ) + } + + func updateApproved(at now: Date, reviewer: String, comment: String) -> Self { + + return .init( + id: id, + indexedDate: indexedDate, + title: title, + point: point, + state: .completed, + executorId: executorId, + executedAt: executedAt, + reviewerId: reviewer, + approvedAt: now, + reviewerComment: comment, expiredAt: expiredAt ) } @@ -47,15 +86,12 @@ struct HouseworkItem: Identifiable, Equatable, Sendable, Hashable, Codable { state: .incomplete, executorId: nil, executedAt: nil, + reviewerId: nil, + approvedAt: nil, + reviewerComment: nil, expiredAt: expiredAt ) } - - func isApprovable(_ userId: String) -> Bool { - - guard let executorId else { return false } - return executorId != userId - } } extension HouseworkItem { @@ -67,7 +103,10 @@ extension HouseworkItem { metaData: DailyHouseworkMetaData, state: HouseworkState = .incomplete, executorId: String? = nil, - executedAt: Date? = nil + executedAt: Date? = nil, + reviewerId: String? = nil, + approvedAt: Date? = nil, + reviewerComment: String? = nil ) { self.init( @@ -78,6 +117,9 @@ extension HouseworkItem { state: state, executorId: executorId, executedAt: executedAt, + reviewerId: reviewerId, + approvedAt: approvedAt, + reviewerComment: reviewerComment, expiredAt: metaData.expiredAt ) } diff --git a/homete/Model/Domain/Cohabitant/Housework/HouseworkListStore.swift b/homete/Model/Domain/Cohabitant/Housework/HouseworkListStore.swift index 193c457b..3eb1259b 100644 --- a/homete/Model/Domain/Cohabitant/Housework/HouseworkListStore.swift +++ b/homete/Model/Domain/Cohabitant/Housework/HouseworkListStore.swift @@ -69,6 +69,7 @@ final class HouseworkListStore { Task.detached { + // TODO: PushNotificationContentにファクトリーメソッドを定義する let notificationContent = PushNotificationContent( title: "新しい家事が登録されました", message: newItem.title @@ -100,6 +101,30 @@ final class HouseworkListStore { } } + func approved(target: HouseworkItem, now: Date, reviwer: Account, comment: String) async throws { + + let targetIndexedDate = target.indexedDate + let targetId = target.id + + guard let targetItem = items.item(targetId, targetIndexedDate) else { + + preconditionFailure("Not found target item(id: \(targetId), indexedDate: \(targetIndexedDate))") + } + + let updatedItem = targetItem.updateApproved(at: now, reviewer: reviwer.id, comment: comment) + try await houseworkClient.insertOrUpdateItem(updatedItem, cohabitantId) + + Task.detached { + + let notificationContent = PushNotificationContent.approvedMessage( + reviwerName: reviwer.userName, + houseworkTitle: target.title, + comment: comment + ) + try await self.cohabitantPushNotificationClient.send(self.cohabitantId, notificationContent) + } + } + func returnToIncomplete(target: HouseworkItem, now: Date) async throws { let targetIndexedDate = target.indexedDate diff --git a/homete/Model/Domain/PushNotificationContent.swift b/homete/Model/Domain/PushNotificationContent.swift index 847c1dd1..f5f23fc4 100644 --- a/homete/Model/Domain/PushNotificationContent.swift +++ b/homete/Model/Domain/PushNotificationContent.swift @@ -9,3 +9,13 @@ struct PushNotificationContent: Equatable { let title: String let message: String } + +extension PushNotificationContent { + + static func approvedMessage(reviwerName: String, houseworkTitle: String, comment: String) -> Self { + return .init( + title: "\(reviwerName)が「\(houseworkTitle)」を承認しました!", + message: comment + ) + } +} diff --git a/homete/Views/Components/PreviewUtil/HouseworkUtil.swift b/homete/Views/Components/PreviewUtil/HouseworkUtil.swift new file mode 100644 index 00000000..00efe0c0 --- /dev/null +++ b/homete/Views/Components/PreviewUtil/HouseworkUtil.swift @@ -0,0 +1,39 @@ +// +// HouseworkUtil.swift +// homete +// +// Created by Taichi Sato on 2026/01/12. +// + +import Foundation + +extension HouseworkItem { + + static func makeForPreview( + id: String = UUID().uuidString, + title: String = "", + point: Int = 10, + indexedDate: HouseworkIndexedDate = .init(value: "2026/01/01"), + expiredAt: Date = .distantFuture, + state: HouseworkState = .incomplete, + executorId: String? = nil, + executedAt: Date? = nil, + reviewerId: String? = nil, + approvedAt: Date? = nil, + reviewerComment: String? = nil + ) -> Self { + + return .init( + id: id, + title: title, + point: point, + metaData: .init(indexedDate: indexedDate, expiredAt: expiredAt), + state: state, + executorId: executorId, + executedAt: executedAt, + reviewerId: reviewerId, + approvedAt: approvedAt, + reviewerComment: reviewerComment + ) + } +} diff --git a/homete/Views/HouseworkApproval/Components/HouseworkItemPropertyListContent.swift b/homete/Views/HouseworkApproval/Components/HouseworkItemPropertyListContent.swift index 71ef17fa..5914718b 100644 --- a/homete/Views/HouseworkApproval/Components/HouseworkItemPropertyListContent.swift +++ b/homete/Views/HouseworkApproval/Components/HouseworkItemPropertyListContent.swift @@ -53,7 +53,7 @@ private extension HouseworkItemPropertyListContent { title: "洗濯", point: 10, metaData: .init( - indexedDate: .init(.init(timeIntervalSince1970: 0)), + indexedDate: .init(value: "1970/01/01"), expiredAt: .init(timeIntervalSince1970: 0) ), executedAt: .distantFuture diff --git a/homete/Views/HouseworkApproval/HouseworkApprovalView.swift b/homete/Views/HouseworkApproval/HouseworkApprovalView.swift index cf022549..1c85137a 100644 --- a/homete/Views/HouseworkApproval/HouseworkApprovalView.swift +++ b/homete/Views/HouseworkApproval/HouseworkApprovalView.swift @@ -9,7 +9,10 @@ import SwiftUI struct HouseworkApprovalView: View { @Environment(CohabitantStore.self) var cohabitantStore + @Environment(HouseworkListStore.self) var houseworkListStore + @Environment(\.loginContext.account) var account @Environment(\.dismiss) var dismiss + @CommonError var commonError @State var inputMessage = "" @@ -98,7 +101,9 @@ private extension HouseworkApprovalView { func actionButtonContent() -> some View { VStack(spacing: .space16) { Button { - // TODO: 家事を完了にする + Task { + await tappedApproveButton() + } } label: { Text("完了にする") .frame(maxWidth: .infinity) @@ -116,13 +121,35 @@ private extension HouseworkApprovalView { } } +// MARK: プレゼンテーションロジック + +private extension HouseworkApprovalView { + + func tappedApproveButton() async { + + do { + + try await houseworkListStore.approved( + target: item, + now: .now, + reviwer: account, + comment: inputMessage + ) + dismiss() + } catch { + + commonError = .init(error: error) + } + } +} + #Preview { HouseworkApprovalView(item: .init( id: "", title: "洗濯", point: 10, metaData: .init( - indexedDate: .init(.init(timeIntervalSince1970: 0)), + indexedDate: .init(value: "1970/01/01"), expiredAt: .init(timeIntervalSince1970: 0) ), executorId: "test", @@ -130,4 +157,5 @@ private extension HouseworkApprovalView { )) .setupEnvironmentForPreview() .environment(CohabitantStore(members: .init(value: [.init(id: "test", userName: "hogehoge")]))) + .environment(HouseworkListStore()) } diff --git a/homete/Views/HouseworkBoardView/HouseworkBoardView.swift b/homete/Views/HouseworkBoardView/HouseworkBoardView.swift index 625b556d..27f8d523 100644 --- a/homete/Views/HouseworkBoardView/HouseworkBoardView.swift +++ b/homete/Views/HouseworkBoardView/HouseworkBoardView.swift @@ -10,7 +10,6 @@ import SwiftUI struct HouseworkBoardView: View { @Environment(\.calendar) var calendar - @Environment(\.locale) var locale @Environment(HouseworkListStore.self) var houseworkListStore @State var navigationPath = AppNavigationPath(path: []) @@ -50,8 +49,7 @@ struct HouseworkBoardView: View { dailyHouseworkList: .makeInitialValue( selectedDate: selectedDate, items: [], - calendar: calendar, - locale: locale + calendar: calendar ) ) } @@ -102,7 +100,7 @@ private extension HouseworkBoardView { houseworkBoardList = .init( dailyList: houseworkListStore.items.value, selectedDate: selectedDate, - locale: locale + calendar: calendar ) } } @@ -123,13 +121,13 @@ private extension HouseworkBoardView { title: "洗濯", point: 20, metaData: .init( - indexedDate: .init(.distantPast), + indexedDate: .init(value: "0001/01/01"), expiredAt: .distantPast ) ) ], metaData: .init( - indexedDate: .init(.distantPast), + indexedDate: .init(value: "0001/01/01"), expiredAt: .now ) ) diff --git a/homete/Views/HouseworkBoardView/SubViews/HouseBoardListRow.swift b/homete/Views/HouseworkBoardView/SubViews/HouseBoardListRow.swift index b494473f..3e6fac6c 100644 --- a/homete/Views/HouseworkBoardView/SubViews/HouseBoardListRow.swift +++ b/homete/Views/HouseworkBoardView/SubViews/HouseBoardListRow.swift @@ -29,7 +29,7 @@ struct HouseBoardListRow: View { title: "洗濯", point: 20, metaData: .init( - indexedDate: .init(.distantFuture), + indexedDate: .init(value: "2026/1/1"), expiredAt: .distantPast ) ) diff --git a/homete/Views/HouseworkBoardView/SubViews/HouseworkBoardListContent.swift b/homete/Views/HouseworkBoardView/SubViews/HouseworkBoardListContent.swift index d237bcad..a2b67e79 100644 --- a/homete/Views/HouseworkBoardView/SubViews/HouseworkBoardListContent.swift +++ b/homete/Views/HouseworkBoardView/SubViews/HouseworkBoardListContent.swift @@ -77,9 +77,24 @@ private extension HouseworkBoardListContent { ), state: .incomplete, list: .init(items: [ - .init(id: "1", title: "洗濯", point: 20, metaData: .init(indexedDate: .init(.now), expiredAt: .now)), - .init(id: "2", title: "掃除", point: 100, metaData: .init(indexedDate: .init(.now), expiredAt: .now)), - .init(id: "3", title: "料理", point: 1, metaData: .init(indexedDate: .init(.now), expiredAt: .now)) + .init( + id: "1", + title: "洗濯", + point: 20, + metaData: .init(indexedDate: .init(value: "2026/1/1"), expiredAt: .now) + ), + .init( + id: "2", + title: "掃除", + point: 100, + metaData: .init(indexedDate: .init(value: "2026/1/1"), expiredAt: .now) + ), + .init( + id: "3", + title: "料理", + point: 1, + metaData: .init(indexedDate: .init(value: "2026/1/1"), expiredAt: .now) + ) ]) ) } diff --git a/homete/Views/HouseworkDetailView/HouseworkDetailView.swift b/homete/Views/HouseworkDetailView/HouseworkDetailView.swift index febe53d9..ae628ca3 100644 --- a/homete/Views/HouseworkDetailView/HouseworkDetailView.swift +++ b/homete/Views/HouseworkDetailView/HouseworkDetailView.swift @@ -100,7 +100,7 @@ private extension HouseworkDetailView { id: "", title: "洗濯", point: 10, - metaData: .init(indexedDate: .init(.distantPast), expiredAt: .distantFuture) + metaData: .init(indexedDate: .init(value: "0001/01/01"), expiredAt: .distantFuture) ) ) } @@ -116,7 +116,7 @@ private extension HouseworkDetailView { id: "", title: "洗濯", point: 10, - metaData: .init(indexedDate: .init(.distantPast), expiredAt: .distantFuture) + metaData: .init(indexedDate: .init(value: "0001/01/01"), expiredAt: .distantFuture) ) ) } diff --git a/homete/Views/HouseworkDetailView/SubViews/HouseworkDetailActionContent.swift b/homete/Views/HouseworkDetailView/SubViews/HouseworkDetailActionContent.swift index d6dd0d1a..698c53ee 100644 --- a/homete/Views/HouseworkDetailView/SubViews/HouseworkDetailActionContent.swift +++ b/homete/Views/HouseworkDetailView/SubViews/HouseworkDetailActionContent.swift @@ -23,7 +23,7 @@ struct HouseworkDetailActionContent: View { case .incomplete: requestReviewButton() case .pendingApproval: - if item.isApprovable(account.id) { + if item.canReview(ownUserId: account.id) { approvalButton() } else { @@ -120,7 +120,7 @@ private extension HouseworkDetailActionContent { id: "", title: "洗濯", point: 10, - metaData: .init(indexedDate: .init(.distantPast), expiredAt: .distantFuture) + metaData: .init(indexedDate: .init(value: "2026/1/1"), expiredAt: .distantFuture) ) ) .environment(HouseworkListStore()) @@ -131,15 +131,12 @@ private extension HouseworkDetailActionContent { isLoading: .constant(false), commonErrorContent: .constant(.initial), account: .init(id: "dummy", userName: "", fcmToken: nil, cohabitantId: nil), - item: .init( - id: "", - indexedDate: .init(.distantPast), + item: .makeForPreview( title: "洗濯", point: 10, + indexedDate: .init(value: "2026/1/1"), state: .pendingApproval, - executorId: "dummy", - executedAt: nil, - expiredAt: .distantPast + executorId: "dummy" ) ) .environment(HouseworkListStore()) @@ -149,16 +146,13 @@ private extension HouseworkDetailActionContent { HouseworkDetailActionContent( isLoading: .constant(false), commonErrorContent: .constant(.initial), - account: .init(id: "dummy", userName: "", fcmToken: nil, cohabitantId: nil), - item: .init( - id: "", - indexedDate: .init(.distantPast), + account: .init(id: "ownAccount", userName: "", fcmToken: nil, cohabitantId: nil), + item: .makeForPreview( title: "洗濯", point: 10, + indexedDate: .init(value: "2026/1/1"), state: .pendingApproval, - executorId: "", - executedAt: nil, - expiredAt: .distantPast + executorId: "executorAccount" ) ) .environment(HouseworkListStore()) diff --git a/homete/Views/HouseworkDetailView/SubViews/HouseworkDetailItemListContent.swift b/homete/Views/HouseworkDetailView/SubViews/HouseworkDetailItemListContent.swift index c0337e9b..2952ee00 100644 --- a/homete/Views/HouseworkDetailView/SubViews/HouseworkDetailItemListContent.swift +++ b/homete/Views/HouseworkDetailView/SubViews/HouseworkDetailItemListContent.swift @@ -46,7 +46,7 @@ struct HouseworkDetailItemListContent: View { id: "", title: "洗濯", point: 10, - metaData: .init(indexedDate: .init(.distantPast), expiredAt: .distantFuture) + metaData: .init(indexedDate: .init(value: "0001/01/01"), expiredAt: .distantFuture) ) ) } @@ -58,7 +58,7 @@ struct HouseworkDetailItemListContent: View { id: "", title: "洗濯", point: 10, - metaData: .init(indexedDate: .init(.distantPast), expiredAt: .distantFuture), + metaData: .init(indexedDate: .init(value: "0001/01/01"), expiredAt: .distantFuture), executorId: "test", executedAt: .distantPast ) diff --git a/homete/Views/RegisterHouseworkView/RegisterHouseworkView.swift b/homete/Views/RegisterHouseworkView/RegisterHouseworkView.swift index ef212422..cca38464 100644 --- a/homete/Views/RegisterHouseworkView/RegisterHouseworkView.swift +++ b/homete/Views/RegisterHouseworkView/RegisterHouseworkView.swift @@ -184,7 +184,7 @@ private extension RegisterHouseworkView { RegisterHouseworkView( dailyHouseworkList: .init( items: [], - metaData: .init(indexedDate: .init(.now), expiredAt: .now) + metaData: .init(indexedDate: .init(value: "2026/1/1"), expiredAt: .now) ) ) .injectAppStorageWithPreview("RegisterHouseworkView") { userDefaults in @@ -205,7 +205,7 @@ private extension RegisterHouseworkView { loadingState: .init(store: .init(isLoading: true)), dailyHouseworkList: .init( items: [], - metaData: .init(indexedDate: .init(.now), expiredAt: .now) + metaData: .init(indexedDate: .init(value: "2026/1/1"), expiredAt: .now) ) ) .environment(HouseworkListStore( diff --git a/hometeTests/Domain/Housework/DailyHouseworkListTest.swift b/hometeTests/Domain/Housework/DailyHouseworkListTest.swift index 7190b2cf..e2e9736a 100644 --- a/hometeTests/Domain/Housework/DailyHouseworkListTest.swift +++ b/hometeTests/Domain/Housework/DailyHouseworkListTest.swift @@ -24,9 +24,9 @@ extension DailyHouseworkListTest.MakeInitialValueCase { func makeInitialValue() throws { // Arrange - let calendar = Calendar(identifier: .gregorian) + let calendar = Calendar.japanese let selectedDate = Date() - let expectedIndexedDate = HouseworkIndexedDate(selectedDate) + let expectedIndexedDate = HouseworkIndexedDate(selectedDate, calendar: calendar) let expectedExpiredAt = try #require(calendar.date(byAdding: .month, value: 1, to: selectedDate)) let expectedList = DailyHouseworkList( @@ -38,8 +38,7 @@ extension DailyHouseworkListTest.MakeInitialValueCase { let list = DailyHouseworkList.makeInitialValue( selectedDate: selectedDate, items: [], - calendar: calendar, - locale: .jp + calendar: calendar ) // Assert @@ -61,7 +60,7 @@ extension DailyHouseworkListTest.IsRegisteredCase { // Arrange let list = DailyHouseworkList( items: inputItems, - metaData: .init(indexedDate: .init(.now), expiredAt: .now) + metaData: .init(indexedDate: .init(.now, calendar: .japanese), expiredAt: .now) ) // Act @@ -91,7 +90,7 @@ extension DailyHouseworkListTest.IsAlreadyRegisteredCase { ] let list = DailyHouseworkList( items: items, - metaData: .init(indexedDate: .init(.now), expiredAt: .now) + metaData: .init(indexedDate: .init(.now, calendar: .japanese), expiredAt: .now) ) // Act diff --git a/hometeTests/Domain/Housework/HouseworkBoardListTest.swift b/hometeTests/Domain/Housework/HouseworkBoardListTest.swift index 7e499fd3..bda8aede 100644 --- a/hometeTests/Domain/Housework/HouseworkBoardListTest.swift +++ b/hometeTests/Domain/Housework/HouseworkBoardListTest.swift @@ -44,7 +44,7 @@ struct HouseworkBoardListTest { let actual = HouseworkBoardList( dailyList: inputList, selectedDate: selectTime, - locale: .jp + calendar: .japanese ) // Assert @@ -63,7 +63,7 @@ struct HouseworkBoardListTest { let houseworkBoardList = HouseworkBoardList( dailyList: [.makeForTest(items: inputHouseworkItem)], selectedDate: .now, - locale: .jp + calendar: .japanese ) // Act diff --git a/hometeTests/Domain/Housework/HouseworkIndexedDate.swift b/hometeTests/Domain/Housework/HouseworkIndexedDate.swift index c7bc9416..c71084b1 100644 --- a/hometeTests/Domain/Housework/HouseworkIndexedDate.swift +++ b/hometeTests/Domain/Housework/HouseworkIndexedDate.swift @@ -9,7 +9,15 @@ import Foundation import Testing @testable import homete -struct HouseworkIndexedDateTest { +enum HouseworkIndexedDateTest { + + struct InitCase {} + struct CalcTargetPeriodCase {} +} + +// MARK: - InitCase + +extension HouseworkIndexedDateTest.InitCase { @Test(arguments: [ Date.distantPast, @@ -18,10 +26,10 @@ struct HouseworkIndexedDateTest { .distantFuture ]) func init_parse_date(inputDate: Date) async throws { - let indexedDate = HouseworkIndexedDate(inputDate) + let indexedDate = HouseworkIndexedDate(inputDate, calendar: .japanese) let expected = inputDate.formatted( - Date.FormatStyle(date: .numeric, time: .omitted) + Date.FormatStyle(date: .numeric, time: .omitted, calendar: .japanese, timeZone: .tokyo) .year(.extended(minimumLength: 4)) .month(.twoDigits) .day(.twoDigits) @@ -30,3 +38,50 @@ struct HouseworkIndexedDateTest { #expect(indexedDate.value == expected) } } + +// MARK: - CalcTargetPeriodCase + +extension HouseworkIndexedDateTest.CalcTargetPeriodCase { + + @Test("指定の日付から指定の期間の日付情報を返す") + func calcTargetPeriod() { + + // Arrange + let calendar = Calendar.japanese + + // Act + let result = HouseworkIndexedDate.calcTargetPeriod( + anchorDate: .dateComponents(year: 2026, month: 2, day: 1), + offsetDays: 2, + calendar: calendar + ) + + // Assert + let expected = [ + ["value": "2026/01/30"], + ["value": "2026/01/31"], + ["value": "2026/02/01"], + ["value": "2026/02/02"], + ["value": "2026/02/03"] + ] + #expect(result == expected) + } + + @Test("指定の期間が0以下の場合、基準日付のみ返す") + func calcTargetPeriod_zero_offset() { + + // Arrange + let calendar = Calendar.japanese + + // Act + let result = HouseworkIndexedDate.calcTargetPeriod( + anchorDate: .dateComponents(year: 2026, month: 1, day: 15), + offsetDays: 0, + calendar: calendar + ) + + // Assert + let expected = [["value": "2026/01/15"]] + #expect(result == expected) + } +} diff --git a/hometeTests/Domain/Housework/HouseworkItemTest.swift b/hometeTests/Domain/Housework/HouseworkItemTest.swift new file mode 100644 index 00000000..4ec1eb20 --- /dev/null +++ b/hometeTests/Domain/Housework/HouseworkItemTest.swift @@ -0,0 +1,197 @@ +// +// HouseworkItemTest.swift +// hometeTests +// +// Created by 佐藤汰一 on 2025/09/08. +// + +import Foundation +import Testing + +@testable import homete + +enum HouseworkItemTest { + + struct CanReviewCase {} + struct UpdateStateCase {} +} + +extension HouseworkItemTest.CanReviewCase { + + @Test( + "担当者が自分以外かつ未完了の場合、レビュー可能", + arguments: [HouseworkState.incomplete, .pendingApproval] + ) + func canReview_notOwnUserAndNotCompleted_returnsTrue(state: HouseworkState) { + + // Arrange + let item = HouseworkItem.makeForTest( + id: 1, + state: state, + executorId: "otherUserId" + ) + + // Act + let result = item.canReview(ownUserId: "ownUserId") + + // Assert + #expect(result == true) + } + + @Test( + "担当者が自分以外でも完了済みの場合、レビュー不可", + arguments: ["otherUserId", nil] + ) + func canReview_completedState_returnsFalse(executorId: String?) { + + // Arrange + let item = HouseworkItem.makeForTest( + id: 1, + state: .completed, + executorId: executorId + ) + + // Act + let result = item.canReview(ownUserId: "ownUserId") + + // Assert + #expect(result == false) + } + + @Test( + "担当者が自分の場合、未完了でもレビュー不可", + arguments: HouseworkState.allCases + ) + func canReview_ownUser_returnsFalse(state: HouseworkState) { + + // Arrange + let ownUserId = "ownUserId" + let item = HouseworkItem.makeForTest( + id: 1, + state: state, + executorId: ownUserId + ) + + // Act + let result = item.canReview(ownUserId: ownUserId) + + // Assert + #expect(result == false) + } +} + +// MARK: - UpdateStateCase + +extension HouseworkItemTest.UpdateStateCase { + + @Test("承認待ち状態に更新すると、state・executorId・executedAtが更新される") + func updatePendingApproval_updatesStateAndExecutorInfo() { + + // Arrange + let indexedDate = Date() + let expiredAt = Date().addingTimeInterval(3600) + let item = HouseworkItem.makeForTest( + id: 1, + indexedDate: indexedDate, + title: "洗濯", + point: 100, + state: .incomplete, + expiredAt: expiredAt + ) + let now = Date() + let changerId = "changerId" + + // Act + let result = item.updatePendingApproval(at: now, changer: changerId) + + // Assert + let expected = HouseworkItem.makeForTest( + id: 1, + indexedDate: indexedDate, + title: "洗濯", + point: 100, + state: .pendingApproval, + executorId: changerId, + executedAt: now, + expiredAt: expiredAt + ) + #expect(result == expected) + } + + @Test("承認すると、state・reviewerId・approvedAt・reviewerCommentが更新される") + func updateApproved_updatesStateAndReviewerInfo() { + + // Arrange + let indexedDate = Date() + let expiredAt = Date().addingTimeInterval(3600) + let executorId = "executorId" + let executedAt = Date().addingTimeInterval(-3600) + let item = HouseworkItem.makeForTest( + id: 1, + indexedDate: indexedDate, + title: "洗濯", + point: 100, + state: .pendingApproval, + executorId: executorId, + executedAt: executedAt, + expiredAt: expiredAt + ) + let now = Date() + let reviewerId = "reviewerId" + let comment = "よくできました" + + // Act + let result = item.updateApproved(at: now, reviewer: reviewerId, comment: comment) + + // Assert + let expected = HouseworkItem.makeForTest( + id: 1, + indexedDate: indexedDate, + title: "洗濯", + point: 100, + state: .completed, + executorId: executorId, + executedAt: executedAt, + reviewerId: reviewerId, + approvedAt: now, + reviewerComment: comment, + expiredAt: expiredAt + ) + #expect(result == expected) + } + + @Test("未完了状態に戻すと、stateがincompleteになり実行者・承認者情報がクリアされる") + func updateIncomplete_clearsExecutorAndReviewerInfo() { + + // Arrange + let indexedDate = Date() + let expiredAt = Date().addingTimeInterval(3600) + let item = HouseworkItem.makeForTest( + id: 1, + indexedDate: indexedDate, + title: "洗濯", + point: 100, + state: .completed, + executorId: "executorId", + executedAt: Date(), + reviewerId: "reviewerId", + approvedAt: Date(), + reviewerComment: "コメント", + expiredAt: expiredAt + ) + + // Act + let result = item.updateIncomplete() + + // Assert + let expected = HouseworkItem.makeForTest( + id: 1, + indexedDate: indexedDate, + title: "洗濯", + point: 100, + state: .incomplete, + expiredAt: expiredAt + ) + #expect(result == expected) + } +} diff --git a/hometeTests/Domain/Housework/HouseworkListStoreTest.swift b/hometeTests/Domain/Housework/HouseworkListStoreTest.swift index 6c0699a1..6d3eb694 100644 --- a/hometeTests/Domain/Housework/HouseworkListStoreTest.swift +++ b/hometeTests/Domain/Housework/HouseworkListStoreTest.swift @@ -65,7 +65,13 @@ struct HouseworkListStoreTest { #expect( store.items == .init(value: [ - .init(items: inputHouseworkList, metaData: .init(indexedDate: .init(.now), expiredAt: now)) + .init( + items: inputHouseworkList, + metaData: .init( + indexedDate: .init(.now, calendar: .japanese), + expiredAt: now + ) + ) ]) ) } @@ -226,18 +232,18 @@ struct HouseworkListStoreTest { @Test("家事削除時は家事を削除するAPIを実行する") func remove() async throws { - + // Arrange - + let inputHouseworkItem = HouseworkItem.makeForTest(id: 1) - + try await confirmation { confirmation in - + let store = HouseworkListStore( houseworkClient: .init(removeItemHandler: { item, cohabitantId in - + // Assert - + #expect(item == inputHouseworkItem) #expect(cohabitantId == inputCohabitantId) confirmation() @@ -246,10 +252,84 @@ struct HouseworkListStoreTest { items: [.makeForTest(items: [inputHouseworkItem])], cohabitantId: inputCohabitantId ) - + // Act - + try await store.remove(inputHouseworkItem) } } + + @Test("家事を承認すると、承認情報を更新しパートナーに通知を送信する") + // swiftlint:disable:next function_body_length + func approved() async throws { + + // Arrange + + let inputHouseworkItem = HouseworkItem.makeForTest( + id: 1, + state: .pendingApproval, + executorId: "executorId", + executedAt: .distantPast + ) + let approvedAt = Date() + let inputReviewer = Account( + id: "reviewerId", + userName: "レビュアー", + fcmToken: nil, + cohabitantId: inputCohabitantId + ) + let inputComment = "お疲れ様でした!" + let updatedHouseworkItem = inputHouseworkItem.updateApproved( + at: approvedAt, + reviewer: inputReviewer.id, + comment: inputComment + ) + let expectedNotificationContent = PushNotificationContent.approvedMessage( + reviwerName: inputReviewer.userName, + houseworkTitle: inputHouseworkItem.title, + comment: inputComment + ) + + await confirmation(expectedCount: 2) { confirmation in + + let _: Void = await withCheckedContinuation { continuation in + + let store = HouseworkListStore( + houseworkClient: .init( + insertOrUpdateItemHandler: { item, cohabitantId in + + // Assert + + #expect(item == updatedHouseworkItem) + #expect(cohabitantId == inputCohabitantId) + confirmation() + } + ), + cohabitantPushNotificationClient: .init { id, content in + + // Assert + + #expect(id == inputCohabitantId) + #expect(content == expectedNotificationContent) + confirmation() + continuation.resume() + }, + items: [.makeForTest(items: [inputHouseworkItem])], + cohabitantId: inputCohabitantId + ) + + // Act + + Task { + + try await store.approved( + target: inputHouseworkItem, + now: approvedAt, + reviwer: inputReviewer, + comment: inputComment + ) + } + } + } + } } diff --git a/hometeTests/TestHelper/CalendarHelper.swift b/hometeTests/TestHelper/CalendarHelper.swift new file mode 100644 index 00000000..fca998a6 --- /dev/null +++ b/hometeTests/TestHelper/CalendarHelper.swift @@ -0,0 +1,17 @@ +// +// CalendarHelper.swift +// homete +// +// Created by Taichi Sato on 2026/01/17. +// + +import Foundation + +extension Calendar { + static var japanese: Self { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = .tokyo + calendar.locale = .jp + return calendar + } +} diff --git a/hometeTests/TestHelper/HouseworkItemHelper.swift b/hometeTests/TestHelper/HouseworkItemHelper.swift index b09f13bb..7517c4d3 100644 --- a/hometeTests/TestHelper/HouseworkItemHelper.swift +++ b/hometeTests/TestHelper/HouseworkItemHelper.swift @@ -18,17 +18,23 @@ extension HouseworkItem { state: HouseworkState = .incomplete, executorId: String? = nil, executedAt: Date? = nil, + reviewerId: String? = nil, + approvedAt: Date? = nil, + reviewerComment: String? = nil, expiredAt: Date = .now ) -> Self { return .init( id: "id\(id.formatted())", - indexedDate: .init(indexedDate), + indexedDate: .init(indexedDate, calendar: .japanese), title: title, point: point, state: state, executorId: executorId, executedAt: executedAt, + reviewerId: reviewerId, + approvedAt: approvedAt, + reviewerComment: reviewerComment, expiredAt: expiredAt ) } @@ -40,6 +46,9 @@ extension HouseworkItem { state: HouseworkState? = nil, executorId: String? = nil, executedAt: Date? = nil, + reviewerId: String? = nil, + approvedAt: Date? = nil, + reviewerComment: String? = nil, expiredAt: Date? = nil ) -> HouseworkItem { @@ -49,6 +58,9 @@ extension HouseworkItem { let inputState = state ?? self.state let inputExecutorId = executorId let inputExecutedAt = executedAt + let inputReviewerId = reviewerId + let inputApprovedAt = approvedAt + let inputReviewerComment = reviewerComment let inputExpiredAt = expiredAt ?? self.expiredAt return .init( @@ -59,6 +71,9 @@ extension HouseworkItem { state: inputState, executorId: inputExecutorId, executedAt: inputExecutedAt, + reviewerId: inputReviewerId, + approvedAt: inputApprovedAt, + reviewerComment: inputReviewerComment, expiredAt: inputExpiredAt ) } diff --git a/hometeTests/TestHelper/TimeZoneHelper.swift b/hometeTests/TestHelper/TimeZoneHelper.swift new file mode 100644 index 00000000..55f9f65d --- /dev/null +++ b/hometeTests/TestHelper/TimeZoneHelper.swift @@ -0,0 +1,13 @@ +// +// TimeZoneHelper.swift +// homete +// +// Created by Taichi Sato on 2026/01/17. +// + +import Foundation + +extension TimeZone { + // swiftlint:disable:next force_unwrapping + static let tokyo = Self.init(identifier: "Asia/Tokyo")! +}