予約サービスのAPIを構築してみる

現在レシートローラーでは様々なアドオンサービスを構築しています。まだリリースとはいきませんが、現在検討している予約サービスのAPIを構築してみたいと思います。いろいろお店や教室で利用いただけるように、サービス化していきたいとは思いますが、まずは既存サービスのサポートを受けるための予約サービスを作ってみます。最終的には他サービスにも連携するような予約APIを想定しています。


# 作るものをざっくり絵にする

先ずは絵にしてみます。この段階では詳細は決まってなくても大丈夫です（持論）。

ざっくりとこんな感じのサービスを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/00bb9e88-9c4c-aaf8-8b48-f2f223e1e492.png)

管理者、サービス、クライアントサーバー、利用者の４人がいる想定です。

1. まず、管理者がカレンダーや予約可能な商品を作成します。　
2. 利用者はカレンダーを選択し、予約可能な日付をクライアントサーバーへリクエストします。
3. クライアントサーバーはサービスに同じく対象カレンダーの予約可能な日付をリクエストします。
4. サービスからの値を加工して利用者に予約可能な日付を表示します。
5. 利用者は予約をしたいスロットを選択し、クライアント経由でサービスに予約をいれます。
6. 予約の確認メールまたは何かしらのお知らせを利用者に送ります、同じく管理者にも送ります。 

というのが、流れと範囲です。

データーの構成としては、ここもざっくりこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/f18586b3-6844-757d-a81e-9f99645846ba.png)

アカウントがあり、組織というグループ要素があり、その中にカレンダーが複数あり、カレンダーの中に予約が複数あるようなイメージです。

ここでいうカレンダーは会議室だったり、飲食店のテーブルだったり、ヨガ教室の部屋だったりになります。各要素ごとに細かな設定項目などはあるかと思いますが、一旦ざっくり構成だけ書いてみます。

# データの定義を書く

作りたいモノの絵をかいたところで、次は具体的に書くデータのモデルを書き起こします。今回はAzure SQLを使って進めます。

## ユーザーと組織を定義
一番大きな枠で、アカウントと組織がありますので、まずはこの2つのモデルから。アカウントに関してはASP.NET Core Identityを最終的には継承します。　ここでいうユーザーは管理者と予約をする利用者の両方になります。利用者と管理者の区別はroleでつけるものとします。

```csharp
 // Represents a user with an optional name, extending the IdentityUser class with additional properties.
 public class UserModel : IdentityUser
 {
     public string? Name { get; set; }  // Optional name of the user.
 }

 // Defines the membership details for an organization including roles and user associations.
 [Table("OrganizationMemberships")]
 public class OrganizationMembershipModel
 {
     // Constructor to initialize an OrganizationMembership instance.
     public OrganizationMembershipModel(string id, string organizationId, string userId, string roleId)
     {
         Id = id;
         OrganizationId = organizationId;
         UserId = userId;
         RoleId = roleId;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the membership.
     public string OrganizationId { get; set; }  // Associated organization identifier.
     public string UserId { get; set; }  // Associated user identifier.
     public string RoleId { get; set; }  // Role identifier within the organization.
 }

 // Represents a role within an organization.
 [Table("OrganizationRoles")]
 public class OrganizationRoleModel
 {
     // Constructor to initialize an OrganizationRole instance.
     public OrganizationRoleModel(string id, string name)
     {
         Id = id;
         Name = name;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the role.
     public string Name { get; set; }  // Name of the role.
 }

 // Represents an organization with its attributes and state.
 [Table("Organizations")]
 public class OrganizationModel
 {
     // Constructor to initialize an Organization instance.
     public OrganizationModel(string id, string name, string createdBy)
     {
         Id = id;
         Name = name;
         CreatedBy = createdBy;
         Created = DateTime.Now;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the organization.
     public string Name { get; set; }  // Name of the organization.
     public DateTime Created { get; set; }  // Date and time the organization was created.
     [MaxLength(36)]
     public string CreatedBy { get; set; }  // User identifier of the creator.
     public bool IsSuspended { get; set; }  // Indicates if the organization is currently suspended.
     public DateTime Suspended { get; set; }  // Date and time the organization was suspended.
     public bool IsDeleted { get; set; }  // Indicates if the organization is marked as deleted.
     public DateTime Deleted { get; set; }  // Date and time the organization was deleted.
 }
```

## カレンダーを定義

次にカレンダー周りを定義していきます。　カレンダーとカレンダーグループ（カレンダーがテーブルや部屋だった場合、カレンダーグループは店舗などを想定）、予約モデルになります。


```csharp
 // Represents a group of calendars, typically used to organize related calendars.
 [Table("CalendarGroups")]
 public class CalendarGroupModel
 {
     // Constructor to initialize a CalendarGroup with its unique identifier and name.
     public CalendarGroupModel(string id, string name)
     {
         Id = id;
         Name = name;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the calendar group, restricted to 36 characters.
     public string Name { get; set; }  // Name of the calendar group.
 }

 // Represents an item or entry within a calendar group, linking specific calendars to their respective groups.
 [Table("CalendarGroupItems")]
 public class CalendarGroupItemModel
 {
     // Constructor to initialize a CalendarGroupItem with identifiers for the item, its calendar, and its group.
     public CalendarGroupItemModel(string id, string calendarId, string calendarGroupId)
     {
         Id = id;
         CalendarId = calendarId;
         CalendarGroupId = calendarGroupId;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the calendar group item, restricted to 36 characters.
     [MaxLength(36)]
     public string CalendarId { get; set; }  // Identifier of the calendar associated with this item, restricted to 36 characters.
     [MaxLength(36)]
     public string CalendarGroupId { get; set; }  // Identifier of the group to which this item belongs, restricted to 36 characters.
 }

 // Represents a calendar, detailing its name, associated organization, and settings like time zone and visibility.
 [Table("Calendars")]
 public class CalendarModel
 {
     // Constructor to initialize a Calendar with essential attributes.
     public CalendarModel(string id, string name, string organizationId, string timeZone, bool isPublic, string createdBy, DateTime created)
     {
         Id = id;
         Name = name;
         OrganizationId = organizationId;
         TimeZone = timeZone;
         IsPublic = isPublic;
         CreatedBy = createdBy;
         Created = created;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the calendar, restricted to 36 characters.
     public string Name { get; set; }  // Name of the calendar.
     public string? Description { get; set; }  // Optional description of the calendar.
     [MaxLength(36)]
     public string OrganizationId { get; set; }  // Identifier of the organization owning this calendar, restricted to 36 characters.
     [MaxLength(36)]
     public string TimeZone { get; set; }  // Time zone in which the calendar operates, restricted to 36 characters.
     [MaxLength(12)]
     public string? Color { get; set; }  // Optional color code for the calendar, restricted to 12 characters.
     public bool IsPublic { get; set; }  // Indicates if the calendar is public.
     public bool IsDeleted { get; set; }  // Indicates if the calendar has been marked as deleted.
     public string? DefaultLocation { get; set; }  // Optional default location for events in the calendar.
     public int MaxAttendees { get; set; }  // Maximum number of attendees per event.
     public int MinAttendees { get; set; }  // Minimum number of attendees required for an event.
     public int TimeScale { get; set; }  // Time scale granularity for events (in minutes).
     public string CreatedBy { get; set; }
     public DateTime Created { get; set; }

 }

 // Represents a reservation within a calendar, detailing the reservation's timeframe, booker, and status.
 [Table("Reservations")]
 public class ReservationModel
 {
     // Constructor to initialize a Reservation with its essential attributes.
     public ReservationModel(string id, string calendarId, string organizationId, string name, DateTime startFrom, DateTime endAt, bool isWholeDay, string bookerId, string status)
     {
         Id = id;
         CalendarId = calendarId;
         OrganizationId = organizationId;
         Name = name;
         StartFrom = startFrom;
         EndAt = endAt;
         IsWholeDay = isWholeDay;
         BookerId = bookerId;
         Status = status;
     }

     [Key, MaxLength(36)]
     public string Id { get; set; }  // Unique identifier for the reservation, restricted to 36 characters.
     public string OrganizationId { get; set; }
     public string CalendarId { get; set; }
     public string Name { get; set; }  // Name of the reservation.
     public string? Description { get; set; }  // Optional description of the reservation.
     [MaxLength(36)]
     public string? Color { get; set; }  // Optional color code for the reservation, restricted to 36 characters.
     [MaxLength(36)]
     public string? CartId { get; set; }  // Optional cart identifier if the reservation is part of a booking system, restricted to 36 characters.
     public DateTime StartFrom { get; set; }  // Start time and date of the reservation.
     public DateTime EndAt { get; set; }  // End time and date of the reservation.
     public bool IsWholeDay { get; set; }
     [MaxLength(36)]
     public string BookerId { get; set; }  // Identifier of the person who booked the reservation, restricted to 36 characters.
     [MaxLength(36)]
     public string Status { get; set; }  // Current status of the reservation, restricted to 36 characters.
     public string? UnderName { get; set; }  // Optional name under which the reservation is booked.
     public DateTime Created { get; set; }  // Creation date and time of the reservation.
    
     public bool IsDeleted { get; set; }  // Indicates if the reservation has been marked as deleted.
     public DateTime Deleted { get; set; }  // Date and time when the reservation was marked as deleted.
 }
```

データの定義ができたら、ApplicationDbContext.cs も更新します。


```csharp
 public class ApplicationDbContext : IdentityDbContext<UserModel>
 {
     public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
   : base(options)
     {
     }

     public DbSet<OrganizationModel> Organizations { get; set; }
     public DbSet<OrganizationRoleModel> OrganizationRoles { get; set; }
     public DbSet<OrganizationMembershipModel> OrganizationMemberships { get; set; }
     public DbSet<CalendarModel> Calendars { get; set; }
     public DbSet<CalendarGroupModel> CalendarGroups { get; set; }
     public DbSet<CalendarGroupItemModel> CalendarGroupItems { get; set; }
     public DbSet<ReservationModel> Reservations { get; set; }
 }
```

その後 `add-migration` `update-database` でデータベースを更新します。

作成されたテーブルを確認して次に進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/6fdde338-ee90-122f-5507-a1883d69abc0.png)


# CRUD 

データの定義ができたので、次はデータに対するアクションを書いていきます。C(Create) R(Read (Search + Get)) U(Update) D(Delete) の順で書いていきます.

## 組織のCRUD

まずは、APIに対するInput/Outputのモデルを定義します。

```csharp
 /// <summary>
 /// Represents a request model for creating an organization.
 /// This model captures all necessary information needed to create a new organization.
 /// </summary>
 public class OrganizationsCreateRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsCreateRequestModel with specified details.
     /// </summary>
     /// <param name="name">The name of the organization to be created.</param>
     /// <param name="createdBy">The identifier of the user creating the organization.</param>
     public OrganizationsCreateRequestModel(string name, string createdBy)
     {
         Name = name;
         CreatedBy = createdBy;
     }

     /// <summary>
     /// Gets or sets the name of the organization.
     /// </summary>
     public string Name { get; set; }

     /// <summary>
     /// Gets or sets the identifier of the user who is creating the organization.
     /// This can be used for tracking who initiated the creation process.
     /// </summary>
     public string CreatedBy { get; set; }
 }

 /// <summary>
 /// Represents the response model returned after creating an organization.
 /// This model encapsulates the details of the newly created organization.
 /// </summary>
 public class OrganizationsCreateResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsCreateResponseModel with the created organization.
     /// </summary>
     /// <param name="organization">The organization that has been successfully created.</param>
     public OrganizationsCreateResponseModel(OrganizationModel organization)
     {
         Organization = organization;
     }

     /// <summary>
     /// Gets or sets the details of the created organization.
     /// </summary>
     public OrganizationModel Organization { get; set; }
 }

 /// <summary>
 /// Represents a view model for an organization.
 /// This model is typically used to format or present data in a specific way for views or API responses.
 /// </summary>
 public class OrganizationViewModel
 {
     /// <summary>
     /// Gets or sets the organization details. This property can be null if no organization data is available.
     /// </summary>
     public OrganizationModel? Organization { get; set; }
 }

 /// <summary>
 /// Represents the search criteria for querying organizations.
 /// This model captures the parameters used to filter and page the search results.
 /// </summary>
 public class OrganizationsSearchRequestModel
 {
     /// <summary>
     /// Gets or sets the keyword used for searching organizations by name or other attributes.
     /// This can be null, in which case the filter should not apply.
     /// </summary>
     public string? Keyword { get; set; }

     /// <summary>
     /// Gets or sets the sorting criteria (e.g., 'Name ASC', 'Name DESC').
     /// This can be null, in which case a default sort should be applied.
     /// </summary>
     public string? Sort { get; set; }

     /// <summary>
     /// Gets or sets the current page number in pagination.
     /// </summary>
     public int CurrentPage { get; set; }

     /// <summary>
     /// Gets or sets the number of items per page in pagination.
     /// </summary>
     public int ItemsPerPage { get; set; }
 }

 /// <summary>
 /// Represents the response model returned from a search query for organizations.
 /// This model includes the list of organizations that match the search criteria along with additional pagination details.
 /// </summary>
 public class OrganizationsSearchResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsSearchResponseModel with the list of organizations.
     /// </summary>
     /// <param name="organizations">The list of organizations that match the search criteria.</param>
     public OrganizationsSearchResponseModel(List<OrganizationViewModel> organizations)
     {
         Organizations = organizations;
     }

     /// <summary>
     /// Gets or sets the keyword used in the search query.
     /// This can be null if no keyword was used.
     /// </summary>
     public string? Keyword { get; set; }

     /// <summary>
     /// Gets or sets the sorting criteria used in the search query.
     /// This can be null if no specific sorting was applied.
     /// </summary>
     public string? Sort { get; set; }

     /// <summary>
     /// Gets or sets the current page number in the search results pagination.
     /// </summary>
     public int CurrentPage { get; set; }

     /// <summary>
     /// Gets or sets the number of items per page in the search results pagination.
     /// </summary>
     public int ItemsPerPage { get; set; }

     /// <summary>
     /// Gets or sets the total number of items that match the search criteria.
     /// </summary>
     public int TotalItems { get; set; }

     /// <summary>
     /// Gets or sets the total number of pages available based on the current pagination settings.
     /// </summary>
     public int TotalPages { get; set; }

     /// <summary>
     /// Gets or sets the list of organization view models that match the search criteria.
     /// </summary>
     public List<OrganizationViewModel> Organizations { get; set; }
 }

 /// <summary>
 /// Represents the response model returned when retrieving an organization by its ID.
 /// This model encapsulates the details of the retrieved organization.
 /// </summary>
 public class OrganizationsGetResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsGetResponseModel with the retrieved organization.
     /// </summary>
     /// <param name="organization">The organization that has been successfully retrieved.</param>
     public OrganizationsGetResponseModel(OrganizationViewModel organization)
     {
         Organization = organization;
     }

     /// <summary>
     /// Gets or sets the details of the retrieved organization.
     /// </summary>
     public OrganizationViewModel Organization { get; set; }
 }

 /// <summary>
 /// Represents the request model for updating an organization.
 /// This model captures the necessary information needed to update an existing organization.
 /// </summary>
 public class OrganizationsUpdateRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsUpdateRequestModel with the specified name.
     /// </summary>
     /// <param name="name">The new name of the organization.</param>
     public OrganizationsUpdateRequestModel(string name)
     {
         Name = name;
     }

     /// <summary>
     /// Gets or sets the name of the organization to be updated.
     /// </summary>
     public string Name { get; set; }
 }

 /// <summary>
 /// Represents the response model returned after updating an organization.
 /// This model encapsulates the details of the updated organization.
 /// </summary>
 public class OrganizationsUpdateResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsUpdateResponseModel with the updated organization.
     /// </summary>
     /// <param name="organization">The organization that has been successfully updated.</param>
     public OrganizationsUpdateResponseModel(OrganizationModel organization)
     {
         Organization = organization;
     }

     /// <summary>
     /// Gets or sets the details of the updated organization.
     /// </summary>
     public OrganizationModel Organization { get; set; }
 }

 /// <summary>
 /// Represents the response model returned after deleting an organization.
 /// This model encapsulates the details of the organization that has been deleted.
 /// </summary>
 public class OrganizationsDeleteResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the OrganizationsDeleteResponseModel with the organization that has been deleted.
     /// </summary>
     /// <param name="deletedOrganization">The organization that has been successfully deleted.</param>
     public OrganizationsDeleteResponseModel(OrganizationModel deletedOrganization)
     {
         DeletedOrganization = deletedOrganization;
     }

     /// <summary>
     /// Gets or sets the details of the deleted organization.
     /// </summary>
     public OrganizationModel DeletedOrganization { get; set; }
 }
 ```


### Create 作成


```csharp
 /// <summary>
 /// Creates a new organization.
 /// </summary>
 /// <param name="request">The organization creation request containing the name of the organization.</param>
 /// <returns>Returns the newly created organization details.</returns>
 /// <response code="200">Returns the newly created organization</response>
 /// <response code="400">If the request is null or invalid</response>
 /// <response code="500">If there is an internal server error</response>
 [HttpPost("/organization")]
 [SwaggerOperation(
     Summary = "Create a new organization",
     Description = "Creates a new organization with the specified name. Requires user authentication.",
     OperationId = "CreateOrganization",
     Tags = new[] { "Organization" }
 )]
 [SwaggerResponse(statusCode: 200, type: typeof(OrganizationsCreateResponseModel), description: "Returns the newly created organization")]
 [SwaggerResponse(statusCode: 400, description: "If the input is null or invalid")]
 [SwaggerResponse(statusCode: 500, description: "If there is an internal server error")]
 public async Task<ActionResult<OrganizationsCreateResponseModel>> CreateOrganizationAsync([FromBody] OrganizationsCreateRequestModel request)
 {
     if (request == null)
     {
         return BadRequest("Request cannot be null");
     }

     // Retrieve the currently authenticated user
     var user = await _userManager.GetUserAsync(User);
     if (user == null)
     {
         return Unauthorized("User must be logged in to create an organization");
     }

     // Create a new organization object with a unique identifier
     var organization = new OrganizationModel(Guid.NewGuid().ToString(), request.Name, user.Id)
     {
         Created = DateTime.UtcNow // Using UTC to avoid timezone issues
     };

     // Add the new organization to the database
     _db.Organizations.Add(organization);

      // Add creater as a member (admin) of the organization
     _db.OrganizationMemberships.Add(new OrganizationMembershipModel(Guid.NewGuid().ToString(), 
     organization.Id, user.Id, "admin"));
     
     await _db.SaveChangesAsync();

     // Prepare the response model with the created organization details
     var result = new OrganizationsCreateResponseModel(organization);

     return Ok(result);
 }
```


### Read (Search) 検索



```csharp
/// <summary>
/// Searches organizations based on the specified criteria.
/// </summary>
/// <param name="request">The search request containing keyword, sort criteria, and pagination details.</param>
/// <returns>A list of organizations that match the search criteria along with pagination details.</returns>
/// <response code="200">Returns a list of organizations that match the search criteria</response>
/// <response code="400">If the pagination parameters are invalid</response>
/// <response code="500">If there is an internal server error</response>
[HttpPost("/organization/search")]
[SwaggerOperation(
    Summary = "Search organizations",
    Description = "Searches for organizations based on keywords, sort criteria, and pagination settings. Requires user authentication.",
    OperationId = "SearchOrganization",
    Tags = new[] { "Organization" }
)]
[SwaggerResponse(statusCode: 200, type: typeof(OrganizationsSearchResponseModel), description: "Successful search results with pagination")]
[SwaggerResponse(statusCode: 400, description: "Invalid pagination parameters")]
[SwaggerResponse(statusCode: 500, description: "Internal server error")]
public ActionResult<OrganizationsSearchResponseModel> SearchOrganization([FromBody] OrganizationsSearchRequestModel request)
{
    // Input validation
    if (request.CurrentPage <= 0)
    {
        return BadRequest("CurrentPage must be greater than 0.");
    }
    if (request.ItemsPerPage <= 0)
    {
        return BadRequest("ItemsPerPage must be greater than 0.");
    }

    // Query organizations based on keyword search
    var organizations = from o in _db.Organizations select o;
    if (!string.IsNullOrEmpty(request.Keyword))
    {
        organizations = organizations.Where(o => o.Name.Contains(request.Keyword));
    }

    // Count total items to support pagination
    int totalItems = organizations.Count();
    var items = organizations
                .Skip(request.ItemsPerPage * (request.CurrentPage - 1))
                .Take(request.ItemsPerPage)
                .Select(r => new OrganizationViewModel { Organization = r })
                .ToList();

    // Prepare the response model with pagination info
    var result = new OrganizationsSearchResponseModel(items)
    {
        Keyword = request.Keyword,
        Sort = request.Sort,
        CurrentPage = request.CurrentPage,
        ItemsPerPage = request.ItemsPerPage,
        TotalItems = totalItems,
    };

    return Ok(result);
}
        
```

### Read (Get) 取得

```csharp
 /// <summary>
 /// Retrieves an organization by its ID.
 /// </summary>
 /// <param name="id">The unique identifier of the organization to retrieve.</param>
 /// <returns>Returns the organization details if found; otherwise, returns a NotFound result.</returns>
 /// <response code="200">Returns the organization details if found</response>
 /// <response code="404">If no organization is found with the provided ID</response>
 [HttpGet("/organization/{id}")]
 [SwaggerOperation(
     Summary = "Retrieve an organization by ID",
     Description = "Retrieves the details of an organization based on the unique identifier provided. Requires user authentication.",
     OperationId = "GetOrganization",
     Tags = new[] { "Organization" }
 )]
 [SwaggerResponse(statusCode: 200, type: typeof(OrganizationsGetResponseModel), description: "Successful retrieval of the organization")]
 [SwaggerResponse(statusCode: 404, description: "The organization with the specified ID was not found")]
 public ActionResult<OrganizationsGetResponseModel> GetOrganization([FromRoute] string id)
 {
     // Query the database for the organization with the specified ID
     var organization = (from o in _db.Organizations
                         where o.Id == id
                         select new OrganizationViewModel
                         {
                             Organization = o
                         }).FirstOrDefault();

     // Check if the organization was found
     if (organization == null)
     {
         return NotFound();
     }

     // Prepare the response model with the found organization details
     var result = new OrganizationsGetResponseModel(organization);

     return Ok(result);
 }
```

### Update 更新

```csharp
/// <summary>
/// Updates an existing organization.
/// </summary>
/// <param name="id">The unique identifier of the organization to update.</param>
/// <param name="request">The request model containing updated fields for the organization.</param>
/// <returns>Returns the updated organization details. If the organization is not found, returns NotFound.</returns>
/// <response code="200">Returns the updated organization details</response>
/// <response code="404">If no organization is found with the provided ID</response>
/// <response code="400">If the request data is invalid</response>
[HttpPut("/organization/{id}")]
[SwaggerOperation(
    Summary = "Update an existing organization",
    Description = "Updates the specified fields of an existing organization. Requires user authentication and the organization ID in the route.",
    OperationId = "UpdateOrganization",
    Tags = new[] { "Organization" }
)]
[SwaggerResponse(statusCode: 200, type: typeof(OrganizationsUpdateResponseModel), description: "Successful update of the organization")]
[SwaggerResponse(statusCode: 404, description: "The organization with the specified ID was not found")]
[SwaggerResponse(statusCode: 400, description: "Invalid data in request")]
public async Task<ActionResult<OrganizationsUpdateResponseModel>> UpdateOrganization(
    [FromRoute] string id,
    [FromBody] OrganizationsUpdateRequestModel request)
{
    // Attempt to find the existing organization by ID
    var original = await _db.Organizations.FindAsync(id);

    // Check if the organization exists
    if (original == null)
    {
        return NotFound();
    }

    // Update the organization's properties
    original.Name = request.Name;
    // Add more fields to update as necessary

    // Save the updated organization back to the database
    _db.Organizations.Update(original);
    await _db.SaveChangesAsync();

    // Prepare the response model with the updated organization details
    var result = new OrganizationsUpdateResponseModel(original);

    return Ok(result);
}
```

### Delete 削除

ここでは物理削除していますが、論理削除でもいいかと思います。

```csharp
/// <summary>
/// Deletes an organization by its ID.
/// </summary>
/// <param name="id">The unique identifier of the organization to be deleted.</param>
/// <returns>Returns a response indicating the result of the deletion process.</returns>
/// <response code="200">Returns the details of the deleted organization</response>
/// <response code="404">If no organization is found with the provided ID</response>
[HttpDelete("/organization/{id}")]
[SwaggerOperation(
    Summary = "Delete an organization",
    Description = "Deletes an existing organization based on the unique identifier provided. Requires user authentication.",
    OperationId = "DeleteOrganization",
    Tags = new[] { "Organization" }
)]
[SwaggerResponse(statusCode: 200, type: typeof(OrganizationsDeleteResponseModel), description: "Successful deletion of the organization")]
[SwaggerResponse(statusCode: 404, description: "The organization with the specified ID was not found")]
public async Task<ActionResult<OrganizationsDeleteResponseModel>> DeleteOrganizationAsync(
    [FromRoute] string id)
{
    // Attempt to find the organization by ID
    var original = await _db.Organizations.FindAsync(id);

    // Check if the organization exists
    if (original == null)
    {
        return NotFound();
    }

    // Remove the organization from the database
    _db.Organizations.Remove(original);
    await _db.SaveChangesAsync();

    // Prepare the response model with the deleted organization details
    var result = new OrganizationsDeleteResponseModel(original);

    return Ok(result);
}

```

Swaggerde確認、一旦よさそうなので進めます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/f87023f7-7f9e-cb9e-ef13-0a340ca979e9.png)


## カレンダーのCRUD

次は組織内にあるカレンダーのCRUDを作ります。

同じくまずは、APIに対するInput/Outputのモデルを定義します。

```csharp 
 /// <summary>
 /// Represents the request model for creating a new calendar.
 /// This model includes all necessary details required to create a calendar within an organization.
 /// </summary>
 public class CalendarCreateRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarCreateRequestModel with specified details.
     /// </summary>
     /// <param name="name">The name of the calendar.</param>
     /// <param name="timeZone">The time zone in which the calendar operates.</param>
     /// <param name="isPublic">Indicates whether the calendar is public or private.</param>
     /// <param name="maxAttendees">The maximum number of attendees allowed per event.</param>
     /// <param name="minAttendees">The minimum number of attendees required for an event.</param>
     /// <param name="timeScale">The time scale in minutes used for calendar events.</param>
     /// <param name="createdBy">The identifier of the user creating the calendar.</param>
     /// <param name="created">The date and time when the calendar was created.</param>
     public CalendarCreateRequestModel(string name, string timeZone,
         bool isPublic, int maxAttendees, int minAttendees, int timeScale, string createdBy,
         DateTime created)
     {
         Name = name;
         TimeZone = timeZone;
         IsPublic = isPublic;
         MaxAttendees = maxAttendees;
         MinAttendees = minAttendees;
         TimeScale = timeScale;
         CreatedBy = createdBy;
         Created = created;
     }

     /// <summary>
     /// Gets or sets the name of the calendar.
     /// </summary>
     public string Name { get; set; }

     /// <summary>
     /// Gets or sets the description of the calendar. Optional.
     /// </summary>
     public string? Description { get; set; }

     /// <summary>
     /// Gets or sets the time zone of the calendar.
     /// </summary>
     public string TimeZone { get; set; }

     /// <summary>
     /// Gets or sets the color associated with the calendar. Optional.
     /// </summary>
     public string? Color { get; set; }

     /// <summary>
     /// Gets or sets a value indicating whether the calendar is public.
     /// </summary>
     public bool IsPublic { get; set; }

     /// <summary>
     /// Gets or sets the default location for events in the calendar. Optional.
     /// </summary>
     public string? DefaultLocation { get; set; }

     /// <summary>
     /// Gets or sets the maximum number of attendees allowed per event.
     /// </summary>
     public int MaxAttendees { get; set; }

     /// <summary>
     /// Gets or sets the minimum number of attendees required for an event.
     /// </summary>
     public int MinAttendees { get; set; }

     /// <summary>
     /// Gets or sets the time scale, in minutes, for events in the calendar.
     /// </summary>
     public int TimeScale { get; set; }

     /// <summary>
     /// Gets or sets the identifier of the user who created the calendar.
     /// </summary>
     public string CreatedBy { get; set; }

     /// <summary>
     /// Gets or sets the date and time when the calendar was created.
     /// </summary>
     public DateTime Created { get; set; }
 }


 /// <summary>
 /// Represents the response model for a calendar creation request.
 /// This model contains the details of the newly created calendar, encapsulated within a CalendarViewModel.
 /// </summary>
 public class CalendarCreateResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarCreateResponseModel with the specified calendar details.
     /// </summary>
     /// <param name="calendar">The calendar view model that includes the details of the newly created calendar.</param>
     public CalendarCreateResponseModel(CalendarViewModel calendar)
     {
         Calendar = calendar;
     }

     /// <summary>
     /// Gets or sets the CalendarViewModel that includes the details of the newly created calendar.
     /// </summary>
     public CalendarViewModel Calendar { get; set; }
 }

 /// <summary>
 /// Represents the search criteria for querying calendars.
 /// This model includes pagination parameters and can optionally include search keywords and sorting instructions.
 /// </summary>
 public class CalendarsSearchRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarsSearchRequestModel with specified pagination details.
     /// </summary>
     /// <param name="currentPage">The page number of the search results to retrieve.</param>
     /// <param name="itemsPerPage">The number of items to display per page in the search results.</param>
     public CalendarsSearchRequestModel(int currentPage, int itemsPerPage)
     {
         CurrentPage = currentPage;
         ItemsPerPage = itemsPerPage;
     }

     /// <summary>
     /// Gets or sets the search keyword to filter the calendars. Optional.
     /// </summary>
     /// <remarks>
     /// If provided, the search will include only calendars that contain this keyword in their searchable fields.
     /// </remarks>
     public string? Keyword { get; set; }

     /// <summary>
     /// Gets or sets the sorting criteria for the search results. Optional.
     /// </summary>
     /// <remarks>
     /// Example formats include "Name asc" or "Created desc". If not provided, a default sort may be applied.
     /// </remarks>
     public string? Sort { get; set; }

     /// <summary>
     /// Gets or sets the current page number of the search results.
     /// </summary>
     public int CurrentPage { get; set; }

     /// <summary>
     /// Gets or sets the number of items per page to be returned in the search results.
     /// </summary>
     public int ItemsPerPage { get; set; }
 }
 /// <summary>
 /// Represents the response model for a search query on calendars.
 /// This model provides a structured format for pagination and includes the results of the search.
 /// </summary>
 public class CalendarsSearchResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarsSearchResponseModel with a list of calendar view models.
     /// </summary>
     /// <param name="calendars">A list of CalendarViewModels that represent the search results.</param>
     public CalendarsSearchResponseModel(List<CalendarViewModel> calendars)
     {
         Items = calendars;
     }

     /// <summary>
     /// Gets or sets the keyword used in the search query. Optional.
     /// </summary>
     /// <remarks>
     /// If provided, this was used to filter the results based on searchable fields within the calendar data.
     /// </remarks>
     public string? Keyword { get; set; }

     /// <summary>
     /// Gets or sets the sorting criteria used in the search query. Optional.
     /// </summary>
     /// <remarks>
     /// Examples include "Name asc" or "Created desc". If not provided, a default sort may have been applied.
     /// </remarks>
     public string? Sort { get; set; }

     /// <summary>
     /// Gets or sets the current page number of the search results, indicating where in the pagination the returned data is situated.
     /// </summary>
     public int CurrentPage { get; set; }

     /// <summary>
     /// Gets or sets the number of items per page that were returned in this segment of the search results.
     /// </summary>
     public int ItemsPerPage { get; set; }

     /// <summary>
     /// Gets or sets the total number of items that matched the search criteria, useful for calculating the total number of pages.
     /// </summary>
     public int TotalItems { get; set; }

     /// <summary>
     /// Gets or sets the total number of pages available based on the current pagination settings.
     /// </summary>
     public int TotalPages { get; set; }

     /// <summary>
     /// Gets or sets the list of CalendarViewModels that represent the search results.
     /// </summary>
     public List<CalendarViewModel> Items { get; set; }
 }

 /// <summary>
 /// Represents the response model for retrieving a calendar.
 /// This model is used to provide detailed information about a specific calendar following a retrieval request.
 /// </summary>
 public class CalendarGetResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarGetResponseModel with the specified calendar view model.
     /// </summary>
     /// <param name="calendar">The calendar view model that includes all relevant details of the retrieved calendar.</param>
     public CalendarGetResponseModel(CalendarViewModel calendar)
     {
         Calendar = calendar;
     }

     /// <summary>
     /// Gets or sets the CalendarViewModel that includes all the details of the retrieved calendar.
     /// </summary>
     public CalendarViewModel Calendar { get; set; }
 }

 /// <summary>
 /// Represents the view model for a calendar.
 /// This model is used primarily for data transfer within the API, especially for encapsulating calendar details in responses.
 /// </summary>
 public class CalendarViewModel
 {
     /// <summary>
     /// Gets or sets the CalendarModel that contains detailed information about the calendar.
     /// This property may be null if the specific calendar details are not available or not necessary for the context in which the view model is used.
     /// </summary>
     public CalendarModel? Calendar { get; set; }
 }

 /// <summary>
 /// Represents the request model for updating an existing calendar.
 /// This model captures all necessary details required to modify a calendar within an organization.
 /// </summary>
 public class CalendarUpdateRequestModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarUpdateRequestModel with specified details.
     /// </summary>
     /// <param name="name">The new name of the calendar.</param>
     /// <param name="organizationId">The ID of the organization to which the calendar belongs.</param>
     /// <param name="timeZone">The time zone in which the calendar operates.</param>
     /// <param name="isPublic">Indicates whether the calendar is public or private.</param>
     /// <param name="maxAttendees">The maximum number of attendees allowed per event.</param>
     /// <param name="minAttendees">The minimum number of attendees required for an event.</param>
     /// <param name="timeScale">The time scale in minutes used for calendar events.</param>
     public CalendarUpdateRequestModel(string name, string organizationId, string timeZone,
         bool isPublic, int maxAttendees, int minAttendees, int timeScale)
     {
         Name = name;
         OrganizationId = organizationId;
         TimeZone = timeZone;
         IsPublic = isPublic;
         MaxAttendees = maxAttendees;
         MinAttendees = minAttendees;
         TimeScale = timeScale;
     }

     /// <summary>
     /// Gets or sets the name of the calendar.
     /// </summary>
     public string Name { get; set; }

     /// <summary>
     /// Gets or sets the optional description of the calendar.
     /// </summary>
     public string? Description { get; set; }

     /// <summary>
     /// Gets or sets the ID of the organization to which the calendar belongs.
     /// </summary>
     public string OrganizationId { get; set; }

     /// <summary>
     /// Gets or sets the time zone of the calendar.
     /// </summary>
     public string TimeZone { get; set; }

     /// <summary>
     /// Gets or sets the optional color associated with the calendar.
     /// </summary>
     public string? Color { get; set; }

     /// <summary>
     /// Gets or sets a value indicating whether the calendar is public.
     /// </summary>
     public bool IsPublic { get; set; }

     /// <summary>
     /// Gets or sets the optional default location for events in the calendar.
     /// </summary>
     public string? DefaultLocation { get; set; }

     /// <summary>
     /// Gets or sets the maximum number of attendees allowed per event.
     /// </summary>
     public int MaxAttendees { get; set; }

     /// <summary>
     /// Gets or sets the minimum number of attendees required for an event.
     /// </summary>
     public int MinAttendees { get; set; }

     /// <summary>
     /// Gets or sets the time scale, in minutes, for events in the calendar.
     /// </summary>
     public int TimeScale { get; set; }
 }

 /// <summary>
 /// Represents the response model for an update request on a calendar.
 /// This model provides the details of the updated calendar, ensuring that the requester can verify the new state of the calendar post-update.
 /// </summary>
 public class CalendarUpdateResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarUpdateResponseModel with the updated calendar details.
     /// </summary>
     /// <param name="calendar">The calendar model that includes all updated details of the calendar.</param>
     public CalendarUpdateResponseModel(CalendarModel calendar)
     {
         Calendar = calendar;
     }

     /// <summary>
     /// Gets or sets the CalendarModel that includes the details of the updated calendar.
     /// </summary>
     public CalendarModel Calendar { get; set; }
 }

 /// <summary>
 /// Represents the response model for a calendar deletion request.
 /// This model provides details of the calendar that has been deleted, allowing for verification and record-keeping.
 /// </summary>
 public class CalendarDeleteResponseModel
 {
     /// <summary>
     /// Initializes a new instance of the CalendarDeleteResponseModel with the specified deleted calendar details.
     /// </summary>
     /// <param name="deletedCalendar">The calendar model that includes all relevant details of the deleted calendar.</param>
     public CalendarDeleteResponseModel(CalendarModel deletedCalendar)
     {
         Calendar = deletedCalendar;
     }

     /// <summary>
     /// Gets or sets the CalendarModel that contains the details of the deleted calendar.
     /// This property provides a final snapshot of the calendar prior to its deletion.
     /// </summary>
     public CalendarModel Calendar { get; set; }
 }
```

### Create 新規作成

```csharp 
 /// <summary>
 /// Creates a new calendar associated with a specified organization.
 /// </summary>
 /// <param name="organizationId">The ID of the organization to which the calendar will be associated.</param>
 /// <param name="request">The details of the calendar to be created.</param>
 /// <returns>Returns the created calendar details or appropriate error messages.</returns>
 /// <remarks>
 /// Sample request:
 ///
 ///     POST /{organizationId}/calendar
 ///     {
 ///        "name": "Team Meetings",
 ///        "timeZone": "Eastern Standard Time",
 ///        "isPublic": true,
 ///        "description": "Calendar for all team meetings",
 ///        "defaultLocation": "Board Room",
 ///        "color": "blue",
 ///        "maxAttendees": 50,
 ///        "minAttendees": 1,
 ///        "timeScale": 15
 ///     }
 ///
 /// </remarks>
 /// <response code="200">Returns the newly created calendar</response>
 /// <response code="400">If the request is null or parameters are missing</response>
 /// <response code="401">If the user is not authenticated</response>
 /// <response code="404">If the specified organization is not found</response>
 [HttpPost("/{organizationId}/calendar")]
 [SwaggerOperation(Summary = "Create a new calendar", Description = "Creates a new calendar associated with a specified organization.")]
 [SwaggerResponse(statusCode: 200, type: typeof(CalendarCreateResponseModel), description: "Successfully created the calendar")]
 [SwaggerResponse(statusCode: 400, description: "Bad request if the request body is null or missing required fields")]
 [SwaggerResponse(statusCode: 401, description: "Unauthorized if the user is not logged in")]
 [SwaggerResponse(statusCode: 404, description: "Not found if no organization matches the provided ID")]
 public async Task<ActionResult<CalendarCreateResponseModel>> CreateCalendarAsync(
     [FromQuery] string organizationId,
     [FromBody] CalendarCreateRequestModel request)
 {
     if (request == null)
     {
         return BadRequest("Request cannot be null");
     }

     // Retrieve the currently authenticated user
     var user = await _userManager.GetUserAsync(User);
     if (user == null)
     {
         return Unauthorized("User must be logged in to create an organization");
     }

     // Make sure the organization exists
     var organization = _db.Organizations.Find(organizationId);
     if (organization == null)
     {
         return NotFound("Organization not found");
     }

     var calendar = new CalendarModel(Guid.NewGuid().ToString(), request.Name,
         organization.Id, request.TimeZone, request.IsPublic, user.Id, DateTime.UtcNow)
     {
         Description = request.Description,
         DefaultLocation = request.DefaultLocation,
         Color = request.Color,
         MaxAttendees = request.MaxAttendees,
         MinAttendees = request.MinAttendees,
         TimeScale = request.TimeScale
     };

     _db.Calendars.Add(calendar);
     await _db.SaveChangesAsync();

     var result = new CalendarCreateResponseModel(new CalendarViewModel()
     {
         Calendar = calendar
     });

     return Ok(result);
 }
```

### Read (Search) 検索

```csharp 
/// <summary>
/// Searches calendars within an organization based on the provided criteria.
/// </summary>
/// <param name="organizationId">The ID of the organization whose calendars are being searched.</param>
/// <param name="request">The search criteria including keyword, pagination, and sorting information.</param>
/// <returns>Returns a list of calendars that match the search criteria along with pagination details.</returns>
/// <remarks>
/// Sample request:
///
///     POST /{organizationId}/calendar/search
///     {
///        "keyword": "Team",
///        "sort": "name",
///        "currentPage": 1,
///        "itemsPerPage": 10
///     }
///
/// </remarks>
/// <response code="200">Returns the list of calendars that match the search criteria</response>
/// <response code="400">If the pagination parameters are invalid</response>
[HttpPost("/{organizationId}/calendar/search")]
[SwaggerOperation(Summary = "Search calendars", Description = "Searches for calendars within an organization based on the provided criteria.")]
[SwaggerResponse(statusCode: 200, type: typeof(CalendarsSearchResponseModel), description: "Successful retrieval of the list of calendars")]
[SwaggerResponse(statusCode: 400, description: "Bad request if the pagination parameters are incorrect")]
public ActionResult<CalendarsSearchResponseModel> SearchCalendar(
    [FromRoute] string organizationId, [FromBody] CalendarsSearchRequestModel request)
{
    // Input validation
    if (request.CurrentPage <= 0)
    {
        return BadRequest("CurrentPage must be greater than 0.");
    }
    if (request.ItemsPerPage <= 0)
    {
        return BadRequest("ItemsPerPage must be greater than 0.");
    }

    var calendars = from o in _db.Calendars where o.OrganizationId == organizationId select o;
    if (!string.IsNullOrEmpty(request.Keyword))
    {
        calendars = calendars.Where(o => o.Name.Contains(request.Keyword));
    }

    // Count total items to support pagination
    int totalItems = calendars.Count();
    var items = calendars
                .Skip(request.ItemsPerPage * (request.CurrentPage - 1))
                .Take(request.ItemsPerPage)
                .Select(r => new CalendarViewModel { Calendar = r })
                .ToList();

    // Prepare the response model with pagination info
    var result = new CalendarsSearchResponseModel(items)
    {
        Keyword = request.Keyword,
        Sort = request.Sort,
        CurrentPage = request.CurrentPage,
        ItemsPerPage = request.ItemsPerPage,
        TotalItems = totalItems,
        TotalPages = (int)Math.Ceiling((double)totalItems / request.ItemsPerPage)
    };

    return Ok(result);
}
```

### Read (Get)　取得

ここではorganizationIdを確認していますが、本来現在ログインしているユーザーが組織にいて、ロールを持っているか確認するべきだと思いますが、その辺りは追々追加していこうと思います。

```csharp 
/// <summary>
/// Retrieves a calendar by its ID within a specified organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to which the calendar belongs.</param>
/// <param name="id">The ID of the calendar to retrieve.</param>
/// <returns>Returns the calendar details if found or a not found error if no calendar is found with the provided ID within the organization.</returns>
/// <response code="200">Returns the calendar details if found</response>
/// <response code="404">If no calendar is found with the specified ID within the organization</response>
[HttpGet("/{organizationId}/calendar/{id}")]
[SwaggerOperation(Summary = "Retrieve a calendar", Description = "Retrieves a calendar by its ID within a specified organization.")]
[SwaggerResponse(statusCode: 200, type: typeof(CalendarGetResponseModel), description: "Successfully retrieved the calendar")]
[SwaggerResponse(statusCode: 404, description: "Not found if no calendar exists with the specified ID within the organization")]
public ActionResult<CalendarGetResponseModel> GetCalendar(
    [FromRoute] string organizationId,
    [FromRoute] string id)
{
    // Query the database for the calendar with the specified ID and ensure it belongs to the specified organization
    var calendar = (from o in _db.Calendars
                    where o.Id == id && o.OrganizationId == organizationId
                    select new CalendarViewModel
                    {
                        Calendar = o
                    }).FirstOrDefault();

    // Check if the calendar was found
    if (calendar == null)
    {
        return NotFound();
    }

    var result = new CalendarGetResponseModel(calendar);

    return Ok(result);
}
```

### Update　更新

```csharp 
/// <summary>
/// Updates a calendar within a specified organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to which the calendar belongs.</param>
/// <param name="id">The ID of the calendar to update.</param>
/// <param name="request">The updated data for the calendar.</param>
/// <returns>Returns the updated calendar details or appropriate error messages.</returns>
/// <response code="200">If the calendar is successfully updated</response>
/// <response code="404">If no calendar is found with the specified ID within the organization</response>
/// <response code="401">If the calendar does not belong to the given organization or the user is not authorized</response>
[HttpPut("/{organizationId}/calendar/{id}")]
[SwaggerOperation(Summary = "Update a calendar", Description = "Updates a specific calendar within a specified organization based on the provided calendar ID.")]
[SwaggerResponse(statusCode: 200, type: typeof(CalendarUpdateResponseModel), description: "Successfully updated the calendar")]
[SwaggerResponse(statusCode: 404, description: "Not found if no calendar exists with the specified ID within the organization")]
[SwaggerResponse(statusCode: 401, description: "Unauthorized if the calendar does not belong to the given organization")]
public async Task<ActionResult<CalendarUpdateResponseModel>> UpdateCalendar(
    [FromRoute] string organizationId, [FromRoute] string id, [FromBody] CalendarUpdateRequestModel request)
{
    // Attempt to find the existing calendar by ID
    var original = await _db.Calendars.FindAsync(id);

    // Check if the calendar exists and belongs to the correct organization
    if (original == null)
    {
        return NotFound("The calendar with the specified ID was not found.");
    }

    if (original.OrganizationId != organizationId)
    {
        return Unauthorized("The calendar does not belong to the specified organization.");
    }

    // Update the calendar's properties
    original.Name = request.Name;
    original.Description = request.Description; // Example of updating more fields
    original.Color = request.Color;
    original.DefaultLocation = request.DefaultLocation;
    original.IsPublic = request.IsPublic;
    original.MaxAttendees = request.MaxAttendees;
    original.MinAttendees = request.MinAttendees;
    original.TimeScale = request.TimeScale;
    original.TimeZone = request.TimeZone;

    // Save the updated calendar back to the database
    _db.Calendars.Update(original);
    await _db.SaveChangesAsync();

    // Prepare the response model with the updated calendar details
    var result = new CalendarUpdateResponseModel(original);

    return Ok(result);
}
        
```

### Delete　削除

```csharp 
/// <summary>
/// Deletes a calendar within a specified organization.
/// </summary>
/// <param name="organizationId">The ID of the organization from which the calendar is to be deleted.</param>
/// <param name="id">The ID of the calendar to delete.</param>
/// <returns>Returns a confirmation of the deletion or appropriate error messages.</returns>
/// <response code="200">If the calendar is successfully deleted</response>
/// <response code="404">If no calendar is found with the specified ID within the organization</response>
/// <response code="401">If the calendar does not belong to the given organization or the user is not authorized</response>
[HttpDelete("/{organizationId}/calendar/{id}")]
[SwaggerOperation(Summary = "Delete a calendar", Description = "Deletes a specific calendar within a specified organization based on the provided calendar ID.")]
[SwaggerResponse(statusCode: 200, type: typeof(CalendarDeleteResponseModel), description: "Successfully deleted the calendar")]
[SwaggerResponse(statusCode: 404, description: "Not found if no calendar exists with the specified ID within the organization")]
[SwaggerResponse(statusCode: 401, description: "Unauthorized if the calendar does not belong to the given organization")]
public async Task<ActionResult<CalendarDeleteResponseModel>> DeleteCalendarAsync(
    [FromRoute] string organizationId,
    [FromRoute] string id)
{
    // Attempt to find the calendar by ID
    var original = await _db.Calendars.FindAsync(id);

    // Check if the calendar exists
    if (original == null)
    {
        return NotFound("The calendar with the specified ID was not found.");
    }

    // Check if the calendar belongs to the specified organization
    if (original.OrganizationId != organizationId)
    {
        return Unauthorized("The calendar does not belong to the specified organization.");
    }

    // Remove the calendar from the database
    _db.Calendars.Remove(original);
    await _db.SaveChangesAsync();

    // Prepare the response model with the deleted calendar details
    var result = new CalendarDeleteResponseModel(original);

    return Ok(result);
}
```

ここで再度Swaggerde確認、一旦よさそうなので進めます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241909/5d8c0163-7c89-4e37-68f9-ec01dd61415d.png)


## 予約のCRUD

次はカレンダー内にある予約のCRUDを作ります。

同じくまずは、APIに対するInput/Outputのモデルを定義します。

```csharp

    /// <summary>
    /// Represents the request model for creating a new reservation.
    /// This model includes all necessary details required to create a reservation within a system.
    /// </summary>
    public class ReservationCreateRequestModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationCreateRequestModel with specified reservation details.
        /// </summary>
        /// <param name="name">The name of the reservation.</param>
        /// <param name="startFrom">The start time and date of the reservation.</param>
        /// <param name="endAt">The end time and date of the reservation.</param>
        /// <param name="isWholeDay">Indicates whether the reservation spans the whole day.</param>
        /// <param name="bookerId">The identifier of the person who made the reservation.</param>
        /// <param name="status">The current status of the reservation.</param>
        /// <param name="created">The date and time when the reservation was created.</param>
        public ReservationCreateRequestModel(string name, DateTime startFrom, DateTime endAt,
            bool isWholeDay, string bookerId, string status, DateTime created)
        {
            Name = name;
            StartFrom = startFrom;
            EndAt = endAt;
            IsWholeDay = isWholeDay;
            BookerId = bookerId;
            Status = status;
            Created = created;
        }

        /// <summary>
        /// Name of the reservation.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Optional description of the reservation.
        /// </summary>
        public string? Description { get; set; }

        /// <summary>
        /// Optional color code for the reservation, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string? Color { get; set; }

        /// <summary>
        /// Optional cart identifier if the reservation is part of a booking system, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string? CartId { get; set; }

        /// <summary>
        /// Start time and date of the reservation.
        /// </summary>
        public DateTime StartFrom { get; set; }

        /// <summary>
        /// End time and date of the reservation.
        /// </summary>
        public DateTime EndAt { get; set; }

        /// <summary>
        /// Indicates whether the reservation spans the entire day.
        /// </summary>
        public bool IsWholeDay { get; set; }

        /// <summary>
        /// Identifier of the person who booked the reservation, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string BookerId { get; set; }

        /// <summary>
        /// Current status of the reservation, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string Status { get; set; }

        /// <summary>
        /// Optional name under which the reservation is booked.
        /// </summary>
        public string? UnderName { get; set; }

        /// <summary>
        /// Creation date and time of the reservation.
        /// </summary>
        public DateTime Created { get; set; }
    }
    
    /// <summary>
    /// Represents the response model for a successfully created reservation.
    /// This model contains the details of the newly created reservation, encapsulated within a ReservationViewModel.
    /// </summary>
    public class ReservationCreateResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationCreateResponseModel with the specified reservation details.
        /// </summary>
        /// <param name="reservation">The reservation view model that includes all relevant details of the newly created reservation.</param>
        public ReservationCreateResponseModel(ReservationViewModel reservation)
        {
            Reservation = reservation;
        }

        /// <summary>
        /// Gets or sets the ReservationViewModel that includes the details of the newly created reservation.
        /// </summary>
        public ReservationViewModel Reservation { get; set; }
    }

    /// <summary>
    /// Represents the request model for searching reservations based on pagination and optional filtering criteria.
    /// This model allows users to query reservations by page and apply filters such as keywords or sorting.
    /// </summary>
    public class ReservationsSearchRequestModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationsSearchRequestModel with specified pagination details.
        /// </summary>
        /// <param name="currentPage">The page number of the search results to retrieve.</param>
        /// <param name="itemsPerPage">The number of items to display per page in the search results.</param>
        public ReservationsSearchRequestModel(int currentPage, int itemsPerPage)
        {
            CurrentPage = currentPage;
            ItemsPerPage = itemsPerPage;
        }

        /// <summary>
        /// Gets or sets the search keyword to filter the reservations. Optional.
        /// </summary>
        /// <remarks>
        /// If provided, the search will include only reservations that contain this keyword in their searchable fields.
        /// This is useful for quickly locating reservations by relevant identifiers or descriptions.
        /// </remarks>
        public string? Keyword { get; set; }

        /// <summary>
        /// Gets or sets the sorting criteria for the search results. Optional.
        /// </summary>
        /// <remarks>
        /// Sort criteria should be specified in the format "FieldName direction", such as "Name asc" or "Created desc".
        /// If not provided, a default sorting may be applied based on the internal logic of the application.
        /// </remarks>
        public string? Sort { get; set; }

        /// <summary>
        /// Gets or sets the current page number of the search results, facilitating pagination.
        /// </summary>
        public int CurrentPage { get; set; }

        /// <summary>
        /// Gets or sets the number of items per page to be returned in the search results.
        /// This helps in managing the volume of data returned and supports efficient navigation through large sets of data.
        /// </summary>
        public int ItemsPerPage { get; set; }
    }

    /// <summary>
    /// Represents the response model for a search operation on reservations.
    /// This model encapsulates the results and pagination details, providing a structured response to search queries.
    /// </summary>
    public class ReservationsSearchResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationsSearchResponseModel with a list of ReservationViewModels.
        /// These models represent the search results, formatted according to the specified search and pagination parameters.
        /// </summary>
        /// <param name="reservations">A list of ReservationViewModels that represent the search results.</param>
        public ReservationsSearchResponseModel(List<ReservationViewModel> reservations)
        {
            Items = reservations;
        }

        /// <summary>
        /// Gets or sets the keyword used in the search query, if any. This is used to filter the results based on searchable fields within the reservation data.
        /// </summary>
        /// <remarks>
        /// Providing a keyword helps to refine search results to only include items that contain the keyword in relevant fields.
        /// </remarks>
        public string? Keyword { get; set; }

        /// <summary>
        /// Gets or sets the sorting criteria used in the search query. This parameter is optional and determines the order of the search results.
        /// </summary>
        /// <remarks>
        /// Examples include "Name asc" or "Created desc". If no sorting parameter is provided, a default sort order may be applied.
        /// </remarks>
        public string? Sort { get; set; }

        /// <summary>
        /// Gets or sets the current page number of the search results. This parameter helps in navigating through paginated data.
        /// </summary>
        public int CurrentPage { get; set; }

        /// <summary>
        /// Gets or sets the number of items per page that were returned in this segment of the search results.
        /// This controls how much data is presented to the user at one time and aids in pagination control.
        /// </summary>
        public int ItemsPerPage { get; set; }

        /// <summary>
        /// Gets or sets the total number of items that matched the search criteria. This is crucial for calculating the total number of pages.
        /// </summary>
        public int TotalItems { get; set; }

        /// <summary>
        /// Gets or sets the total number of pages available, calculated based on the total number of items and the number of items per page.
        /// </summary>
        public int TotalPages { get; set; }

        /// <summary>
        /// Gets or sets the list of ReservationViewModels that represent the search results. Each model contains details of a reservation found in the search.
        /// </summary>
        public List<ReservationViewModel> Items { get; set; }
    }

    /// <summary>
    /// Represents the response model for retrieving a reservation.
    /// This model is used to provide detailed information about a specific reservation following a retrieval request.
    /// </summary>
    public class ReservationGetResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationGetResponseModel with the specified reservation view model.
        /// This constructor sets up the response model with all relevant details of the retrieved reservation.
        /// </summary>
        /// <param name="reservation">The reservation view model that includes all relevant details of the retrieved reservation.</param>
        public ReservationGetResponseModel(ReservationViewModel reservation)
        {
            Reservation = reservation;
        }

        /// <summary>
        /// Gets or sets the ReservationViewModel that includes all the details of the retrieved reservation.
        /// This property provides access to detailed attributes and values of the reservation, such as date, time, participants, and status, among others.
        /// </summary>
        public ReservationViewModel Reservation { get; set; }
    }

    /// <summary>
    /// Represents a view model for a reservation. This model is typically used to encapsulate the reservation data 
    /// that is transferred between the backend and the frontend layers, making it easier to manage data transformations 
    /// and customizations specific to the view requirements.
    /// </summary>
    public class ReservationViewModel
    {
        /// <summary>
        /// Gets or sets the ReservationModel. This property may contain the detailed information of a reservation, 
        /// including dates, times, participant details, and other relevant reservation metadata.
        /// </summary>
        /// <remarks>
        /// The property is nullable, meaning that there may be contexts where a reservation detail is not required 
        /// or not available. This flexibility allows the view model to be used in a variety of scenarios, such as 
        /// creating new reservations or updating existing ones without fully specifying all details initially.
        /// </remarks>
        public ReservationModel? Reservation { get; set; }
    }

    /// <summary>
    /// Represents the request model for updating an existing reservation.
    /// This model captures all necessary details required to modify a reservation within an organization,
    /// including scheduling details, participant information, and reservation status.
    /// </summary>
    public class ReservationUpdateRequestModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationUpdateRequestModel with mandatory parameters for updating a reservation.
        /// </summary>
        /// <param name="name">Name of the reservation.</param>
        /// <param name="startFrom">Start time and date of the reservation.</param>
        /// <param name="endAt">End time and date of the reservation.</param>
        /// <param name="isWholeDay">Indicates whether the reservation spans the entire day.</param>
        /// <param name="bookerId">Identifier of the person who booked the reservation.</param>
        /// <param name="status">Current status of the reservation.</param>
        public ReservationUpdateRequestModel(string name, DateTime startFrom, DateTime endAt,
            bool isWholeDay, string bookerId, string status)
        {
            Name = name;
            StartFrom = startFrom;
            EndAt = endAt;
            IsWholeDay = isWholeDay;
            BookerId = bookerId;
            Status = status;
        }

        /// <summary>
        /// Name of the reservation.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Optional description providing additional details about the reservation.
        /// </summary>
        public string? Description { get; set; }

        /// <summary>
        /// Optional color code for the reservation, used for visual identification, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string? Color { get; set; }

        /// <summary>
        /// Optional cart identifier, used if the reservation is part of a booking system, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string? CartId { get; set; }

        /// <summary>
        /// Start time and date of the reservation, defining when the reservation begins.
        /// </summary>
        public DateTime StartFrom { get; set; }

        /// <summary>
        /// End time and date of the reservation, defining when the reservation concludes.
        /// </summary>
        public DateTime EndAt { get; set; }

        /// <summary>
        /// Indicates whether the reservation is booked for the entire day.
        /// </summary>
        public bool IsWholeDay { get; set; }

        /// <summary>
        /// Identifier of the person who made the reservation, providing a link to the responsible party, restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string BookerId { get; set; }

        /// <summary>
        /// Current status of the reservation, such as 'Confirmed', 'Cancelled', etc., restricted to 36 characters.
        /// </summary>
        [MaxLength(36)]
        public string Status { get; set; }

        /// <summary>
        /// Optional name under which the reservation is booked, providing an alternative reference or alias for the booking.
        /// </summary>
        public string? UnderName { get; set; }

        /// <summary>
        /// Indicates if the reservation has been marked as deleted, providing a flag for soft deletion scenarios.
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// Date and time when the reservation was marked as deleted, used in tracking changes and managing records.
        /// </summary>
        public DateTime Deleted { get; set; }
    }
    /// <summary>
    /// Represents the response model for an update request on a reservation.
    /// This model provides the details of the updated reservation, ensuring that the requester can verify the new state of the reservation post-update.
    /// </summary>
    public class ReservationUpdateResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationUpdateResponseModel with the updated reservation details.
        /// </summary>
        /// <param name="reservation">The reservation model that includes all updated details of the reservation.</param>
        public ReservationUpdateResponseModel(ReservationModel reservation)
        {
            Reservation = reservation;
        }

        /// <summary>
        /// Gets or sets the ReservationModel that includes the details of the updated reservation.
        /// This property encapsulates the entire set of reservation details after they have been modified,
        /// allowing for a comprehensive view of the updated reservation state.
        /// </summary>
        public ReservationModel Reservation { get; set; }
    }

    /// <summary>
    /// Represents the response model for a reservation deletion request.
    /// This model provides details of the reservation that has been deleted, allowing for verification and record-keeping.
    /// </summary>
    public class ReservationDeleteResponseModel
    {
        /// <summary>
        /// Initializes a new instance of the ReservationDeleteResponseModel with the specified deleted reservation details.
        /// This constructor sets up the model with a snapshot of the reservation as it was just before deletion, 
        /// aiding in confirming the correct reservation was removed and providing a record for audit purposes.
        /// </summary>
        /// <param name="deletedReservation">The reservation model that includes all relevant details of the deleted reservation.</param>
        public ReservationDeleteResponseModel(ReservationModel deletedReservation)
        {
            Reservation = deletedReservation;
        }

        /// <summary>
        /// Gets or sets the ReservationModel that contains the details of the deleted reservation.
        /// This property provides a final snapshot of the reservation prior to its deletion, useful for logging, auditing, or
        /// other post-deletion processes that might require a record of the reservation's final state.
        /// </summary>
        public ReservationModel Reservation { get; set; }
    }
```

### Create 作成

```csharp
/// <summary>
/// Creates a new reservation within a specified calendar and organization.
/// </summary>
/// <param name="organizationId">The ID of the organization under which the reservation is made.</param>
/// <param name="calendarId">The ID of the calendar under which the reservation is made.</param>
/// <param name="request">The reservation details.</param>
/// <returns>Returns the created reservation details or appropriate error messages.</returns>
/// <remarks>
/// Sample request:
///
///     POST /{organizationId}/calendar/{calendarId}/reservation
///     {
///         "name": "Board Meeting",
///         "startFrom": "2024-05-12T14:00:00",
///         "endAt": "2024-05-12T15:00:00",
///         "isWholeDay": false,
///         "status": "Confirmed",
///         "description": "Annual board meeting",
///         "cartId": "cart123",
///         "color": "blue",
///         "underName": "John Doe"
///     }
///
/// </remarks>
/// <response code="200">Returns the newly created reservation</response>
/// <response code="400">If the request body is null</response>
/// <response code="401">If the user is not authenticated</response>
/// <response code="404">If the specified organization does not exist</response>
[HttpPost("/{organizationId}/calendar/{calendarId}/reservation")]
[SwaggerOperation(Summary = "Create a new reservation", Description = "Creates a new reservation within a specified calendar and organization.")]
[SwaggerResponse(statusCode: 200, type: typeof(ReservationCreateResponseModel), description : "Successfully created the reservation")]
[SwaggerResponse(statusCode: 400, description : "Bad request if the request body is null")]
[SwaggerResponse(statusCode: 401, description : "Unauthorized if the user is not logged in")]
[SwaggerResponse(statusCode: 404, description : "Not found if the specified organization or calendar does not exist")]
public async Task<ActionResult<ReservationCreateResponseModel>> CreateReservationAsync(
  [FromRoute] string organizationId, [FromRoute] string calendarId,
  [FromBody] ReservationCreateRequestModel request)
{
    if (request == null)
    {
        return BadRequest("Request cannot be null");
    }

    var user = await _userManager.GetUserAsync(User);
    if (user == null)
    {
        return Unauthorized("User must be logged in to create a reservation");
    }

    var organization = _db.Organizations.Find(organizationId);
    if (organization == null)
    {
        return NotFound("Organization not found");
    }

    var reservation = new ReservationModel(Guid.NewGuid().ToString(),
        calendarId, organizationId,
        request.Name,
        request.StartFrom, request.EndAt, request.IsWholeDay, user.Id, request.Status)
    {
        Description = request.Description,
        CartId = request.CartId,
        Color = request.Color,
        UnderName = request.UnderName,
    };

    _db.Reservations.Add(reservation);
    await _db.SaveChangesAsync();

    var result = new ReservationCreateResponseModel(new ReservationViewModel()
    {
        Reservation = reservation
    });

    return Ok(result);
}
```

### Read (Search) 検索

```csharp
  /// <summary>
  /// Searches for reservations within a specific calendar and organization based on the provided search criteria.
  /// </summary>
  /// <param name="organizationId">The ID of the organization to which the calendar belongs.</param>
  /// <param name="calendarId">The ID of the calendar within which to search for reservations.</param>
  /// <param name="request">The search parameters including pagination details, optional keyword, and sort criteria.</param>
  /// <returns>A list of reservations that match the search criteria along with pagination details.</returns>
  /// <remarks>
  /// Sample request:
  ///
  ///     POST /{organizationId}/calendar/{calendarId}/reservation/search
  ///     {
  ///         "keyword": "meeting",
  ///         "sort": "name asc",
  ///         "currentPage": 1,
  ///         "itemsPerPage": 10
  ///     }
  ///
  /// </remarks>
  /// <response code="200">Returns the list of matching reservations along with pagination info</response>
  /// <response code="400">If the pagination parameters are invalid</response>
  [HttpPost("/{organizationId}/calendar/{calendarId}/reservation/search")]
  [SwaggerOperation(Summary = "Search reservations", Description = "Performs a search for reservations within a specified calendar and organization based on search criteria.")]
  [SwaggerResponse(statusCode: 200, type: typeof(ReservationsSearchResponseModel), description : "Successful retrieval of reservation list with pagination")]
  [SwaggerResponse(statusCode: 400, description : "Invalid request parameters")]
  public ActionResult<ReservationsSearchResponseModel> SearchReservation(
      [FromRoute] string organizationId, [FromRoute] string calendarId, [FromBody] ReservationsSearchRequestModel request)
  {
      // Input validation
      if (request.CurrentPage <= 0)
      {
          return BadRequest("CurrentPage must be greater than 0.");
      }
      if (request.ItemsPerPage <= 0)
      {
          return BadRequest("ItemsPerPage must be greater than 0.");
      }

      var reservations = from o in _db.Reservations
                         where o.OrganizationId == organizationId && o.CalendarId == calendarId
                         select o;

      if (!string.IsNullOrEmpty(request.Keyword))
      {
          reservations = reservations.Where(o => o.Name.Contains(request.Keyword));
      }

      // Count total items to support pagination
      int totalItems = reservations.Count();
      var items = reservations
                  .Skip(request.ItemsPerPage * (request.CurrentPage - 1))
                  .Take(request.ItemsPerPage)
                  .Select(r => new ReservationViewModel { Reservation = r })
                  .ToList();

      // Prepare the response model with pagination info
      var result = new ReservationsSearchResponseModel(items)
      {
          Keyword = request.Keyword,
          Sort = request.Sort,
          CurrentPage = request.CurrentPage,
          ItemsPerPage = request.ItemsPerPage,
          TotalItems = totalItems,
          TotalPages = (int)Math.Ceiling((double)totalItems / request.ItemsPerPage)
      };

      return Ok(result);
  }
        
```

### Read (Get) 取得

```csharp
/// <summary>
/// Retrieves a specific reservation by its ID within a given organization and calendar.
/// </summary>
/// <param name="organizationId">The ID of the organization to which the reservation belongs.</param>
/// <param name="calendarId">The ID of the calendar in which the reservation is scheduled.</param>
/// <param name="id">The unique identifier of the reservation to retrieve.</param>
/// <returns>Returns the detailed information of the reservation if found, or an error if not found.</returns>
/// <remarks>
/// Sample request:
///
///     GET /{organizationId}/calendar/{calendarId}/reservation/{id}
///
/// </remarks>
/// <response code="200">Returns the detailed information of the reservation</response>
/// <response code="404">If the reservation is not found within the specified organization and calendar</response>
[HttpGet("/{organizationId}/calendar/{calendarId}/reservation/{id}")]
[SwaggerOperation(Summary = "Retrieve a reservation", Description = "Retrieves a specific reservation by its ID within a given organization and calendar.")]
[SwaggerResponse(statusCode: 200, type: typeof(ReservationGetResponseModel), description : "Successfully retrieved the reservation")]
[SwaggerResponse(statusCode: 404, description : "Reservation not found")]
public ActionResult<ReservationGetResponseModel> GetReservation(
    [FromRoute] string organizationId, [FromRoute] string calendarId,
    [FromRoute] string id)
{
    // Query the database for the reservation with the specified ID and ensure it belongs to the specified organization and calendar
    var reservation = (from o in _db.Reservations
                       where o.Id == id && o.OrganizationId == organizationId &&
                             o.CalendarId == calendarId
                       select new ReservationViewModel
                       {
                           Reservation = o
                       }).FirstOrDefault();

    // Check if the reservation was found
    if (reservation == null)
    {
        return NotFound("The specified reservation was not found within the given organization and calendar.");
    }

    var result = new ReservationGetResponseModel(reservation);

    return Ok(result);
}

```

### Update 更新

```csharp
 /// <summary>
 /// Updates an existing reservation within a specified calendar and organization.
 /// </summary>
 /// <param name="organizationId">The ID of the organization to which the reservation belongs.</param>
 /// <param name="calendarId">The ID of the calendar in which the reservation is scheduled.</param>
 /// <param name="id">The unique identifier of the reservation to update.</param>
 /// <param name="request">The updated reservation details.</param>
 /// <returns>Returns the updated reservation details or appropriate error messages if the reservation cannot be found or updated.</returns>
 /// <remarks>
 /// Sample request:
 ///
 ///     PUT /{organizationId}/calendar/{calendarId}/reservation/{id}
 ///     {
 ///         "name": "Updated Meeting",
 ///         "description": "Updated description here",
 ///         "color": "green",
 ///         "bookerId": "user123",
 ///         "cartId": "cart456",
 ///         "deleted": false,
 ///         "endAt": "2024-05-20T15:00:00",
 ///         "isDeleted": false,
 ///         "isWholeDay": false,
 ///         "startFrom": "2024-05-20T14:00:00",
 ///         "status": "Confirmed",
 ///         "underName": "Jane Doe"
 ///     }
 ///
 /// </remarks>
 /// <response code="200">Successfully updated the reservation</response>
 /// <response code="404">If the reservation, organization, or calendar is not found</response>
 [HttpPut("/{organizationId}/calendar/{calendarId}/reservation/{id}")]
 [SwaggerOperation(Summary = "Update a reservation", Description = "Updates an existing reservation within a specified calendar and organization.")]
 [SwaggerResponse(statusCode: 200, type: typeof(ReservationUpdateResponseModel), description : "Successfully updated the reservation")]
 [SwaggerResponse(statusCode: 404, description : "Not found if the specified reservation, organization, or calendar does not exist")]
 public async Task<ActionResult<ReservationUpdateResponseModel>> UpdateReservation(
     [FromRoute] string organizationId,
     [FromRoute] string calendarId,
     [FromRoute] string id, [FromBody] ReservationUpdateRequestModel request)
 {
     var original = await _db.Reservations.FindAsync(id);

     if (original == null)
     {
         return NotFound("The reservation with the specified ID was not found.");
     }

     if (original.OrganizationId != organizationId)
     {
         return NotFound("The reservation does not belong to the specified organization.");
     }

     if (original.CalendarId != calendarId)
     {
         return NotFound("The reservation does not belong to the specified calendar.");
     }

     original.Name = request.Name;
     original.Description = request.Description;
     original.Color = request.Color;
     original.BookerId = request.BookerId;
     original.CartId = request.CartId;
     original.Deleted = request.Deleted;
     original.EndAt = request.EndAt;
     original.IsDeleted = request.IsDeleted;
     original.IsWholeDay = request.IsWholeDay;
     original.StartFrom = request.StartFrom;
     original.Status = request.Status;
     original.UnderName = request.UnderName;

     _db.Reservations.Update(original);
     await _db.SaveChangesAsync();

     var result = new ReservationUpdateResponseModel(original);

     return Ok(result);
 }
```

### Delete 削除

```csharp
/// <summary>
/// Deletes a reservation within a specified calendar and organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to which the reservation belongs.</param>
/// <param name="calendarId">The ID of the calendar from which the reservation is to be deleted.</param>
/// <param name="id">The unique identifier of the reservation to delete.</param>
/// <returns>Returns a confirmation of the deletion or an error message if the reservation cannot be found or is unauthorized.</returns>
/// <remarks>
/// This method deletes the reservation and returns the details of the deleted reservation.
/// </remarks>
/// <response code="200">Successfully deleted the reservation and returns the deleted reservation details</response>
/// <response code="404">If the reservation is not found within the specified organization and calendar</response>
/// <response code="401">If the reservation does not belong to the specified organization or calendar</response>
[HttpDelete("/{organizationId}/calendar/{calendarId}/reservation/{id}")]
[SwaggerOperation(Summary = "Delete a reservation", Description = "Deletes a specific reservation within a specified calendar and organization.")]
[SwaggerResponse(statusCode: 200, type: typeof(ReservationDeleteResponseModel), description : "Successfully deleted the reservation")]
[SwaggerResponse(statusCode: 404, description : "Not found if the specified reservation does not exist")]
[SwaggerResponse(statusCode: 401, description : "Unauthorized if the reservation does not belong to the specified organization or calendar")]
public async Task<ActionResult<ReservationDeleteResponseModel>> DeleteReservationAsync(
    [FromRoute] string organizationId, [FromRoute] string calendarId,
    [FromRoute] string id)
{
    var original = await _db.Reservations.FindAsync(id);

    if (original == null)
    {
        return NotFound("The reservation with the specified ID was not found.");
    }

    if (original.OrganizationId != organizationId)
    {
        return Unauthorized("The reservation does not belong to the specified organization.");
    }

    if (original.CalendarId != calendarId)
    {
        return Unauthorized("The reservation does not belong to the specified calendar.");
    }

    _db.Reservations.Remove(original);
    await _db.SaveChangesAsync();

    var result = new ReservationDeleteResponseModel(original);

    return Ok(result);
}
```

再度ここでSwaggerを確認。　OKそうなので、実際にコールしてみます。

![uploading...0]()


# カレンダーを作って表示してみる

では、早速作ったAPIを使ってカレンダーを表示してみましょう。

(local環境で動かしています)

## ログイン

まずは、`login`して`token`を取得します。

リクエスト

```
curl -X 'POST' \
  'https://localhost:7135/login' \
  -H 'accept: */*' \
  -H 'Content-Type: application/json' \
  -d '{
  "username": "{username}",
  "password": "{password}"
}'
```

レスポンス

```json
{
  "token": "{token}"
}

```

## 組織を作成

まだデータベースになにも入っていないので、組織から作ります。

リクエスト
```
curl -X 'POST' \
  'https://localhost:7135/organization' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "（株）レシートローラー"
}'
```

レスポンス
```json
{
  "organization": {
    "id": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
    "name": "（株）レシートローラー",
    "created": "2024-05-12T20:21:17.4886117+09:00",
    "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
    "isSuspended": false,
    "suspended": "0001-01-01T00:00:00",
    "isDeleted": false,
    "deleted": "0001-01-01T00:00:00"
  },
  "organizationMembers": [
    {
      "id": "868e5cb1-4c9b-495e-9779-ad6b976eb85b",
      "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
      "userId": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
      "roleId": "admin"
    }
  ]
}
```

## サポート用のカレンダーを作成

リクエスト

```
curl -X 'POST' \
  'https://localhost:7135/e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd/calendar' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "レシートローラーアプリ導入サポートカレンダー",
  "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
  "timeZone": "JST",
  "isPublic": true,
  "defaultLocation": "Online",
  "maxAttendees": 100,
  "minAttendees": 1,
  "timeScale": 15
}'
```

レスポンス

```json
{
  "calendar": {
    "calendar": {
      "id": "7e826091-6147-43e5-aea2-7ee444b56eec",
      "name": "レシートローラーアプリ導入サポートカレンダー",
      "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
      "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
      "timeZone": "JST",
      "color": null,
      "isPublic": true,
      "isDeleted": false,
      "defaultLocation": "Online",
      "maxAttendees": 100,
      "minAttendees": 1,
      "timeScale": 15,
      "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
      "created": "2024-05-12T11:30:12.6164258Z"
    }
  }
}
```
## カレンダーを表示してみる

まだ予約はいれていないので、一旦カレンダー一覧を表示し、カレンダーの詳細を取得してみます。

リクエスト

```
curl -X 'POST' \
  'https://localhost:7135/e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd/calendar/search' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{
  "currentPage": 1,
  "itemsPerPage": 100
}'
```

レスポンス

```json
{
  "keyword": null,
  "sort": null,
  "currentPage": 1,
  "itemsPerPage": 100,
  "totalItems": 1,
  "totalPages": 1,
  "items": [
    {
      "calendar": {
        "id": "7e826091-6147-43e5-aea2-7ee444b56eec",
        "name": "レシートローラーアプリ導入サポートカレンダー",
        "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
        "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
        "timeZone": "JST",
        "color": null,
        "isPublic": true,
        "isDeleted": false,
        "defaultLocation": "Online",
        "maxAttendees": 100,
        "minAttendees": 1,
        "timeScale": 15,
        "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
        "created": "2024-05-12T11:30:12.6164258"
      },
      "numOfValidReservations": 0,
      "reservations": null
    }
  ]
}
```

単体で取得する際には `reservations`を返すようにしています。こちらは実際に予約を入れた後に試してみます。

# 予約を入れてみる

では、予約を入れてみます。　予約時間の確認であったりとvalidationは追加するべきだとは思いますが、一旦はシンプルに予約のデータを書き込んでみます。

リクエスト

```
curl -X 'POST' \
  'https://localhost:7135/e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd/calendar/7e826091-6147-43e5-aea2-7ee444b56eec/reservation' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "テスト予約",
  "description": "テストで予約してみます。",
  "startFrom": "2024-05-15T09:00:00",
  "endAt": "2024-05-15T09:15:00",
  "isWholeDay": false,
  "status": "New",
  "underName": "SHO SHIMODA"
}'
```

レスポンス

```json
{
  "reservation": {
    "reservation": {
      "id": "b927571f-46be-44be-9071-ef121f8007e8",
      "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
      "calendarId": "7e826091-6147-43e5-aea2-7ee444b56eec",
      "name": "テスト予約",
      "description": "テストで予約してみます。",
      "color": null,
      "cartId": null,
      "startFrom": "2024-05-15T09:00:00",
      "endAt": "2024-05-15T09:15:00",
      "isWholeDay": false,
      "bookerId": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
      "status": "New",
      "underName": "SHO SHIMODA",
      "created": "2024-05-12T21:22:20.0816013+09:00",
      "isDeleted": false,
      "deleted": "0001-01-01T00:00:00"
    }
  }
}
```

正常に予約ができました。　では、もう１個予約を入れてからカレンダーのデータをリクエストしてみます。

リクエスト

```
{
  "keyword": null,
  "sort": null,
  "currentPage": 1,
  "itemsPerPage": 100,
  "totalItems": 1,
  "totalPages": 1,
  "items": [
    {
      "calendar": {
        "id": "7e826091-6147-43e5-aea2-7ee444b56eec",
        "name": "レシートローラーアプリ導入サポートカレンダー",
        "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
        "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
        "timeZone": "JST",
        "color": null,
        "isPublic": true,
        "isDeleted": false,
        "defaultLocation": "Online",
        "maxAttendees": 100,
        "minAttendees": 1,
        "timeScale": 15,
        "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
        "created": "2024-05-12T11:30:12.6164258"
      },
      "numOfValidReservations": 2,
      "reservations": null
    }
  ]
}
```

`numOfValidReservation` が正しく、2になっています。

次に詳細をリクエストしてみます。


リクエスト
```
curl -X 'GET' \
  'https://localhost:7135/e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd/calendar/7e826091-6147-43e5-aea2-7ee444b56eec' \
  -H 'accept: text/plain' \
  -H 'Authorization: Bearer {token}'
```

レスポンス 

```json
{
  "calendar": {
    "calendar": {
      "id": "7e826091-6147-43e5-aea2-7ee444b56eec",
      "name": "レシートローラーアプリ導入サポートカレンダー",
      "description": "レシートローラーのアプリ導入サポートを必要な方々がオンラインサポート予約するカレンダー",
      "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
      "timeZone": "JST",
      "color": null,
      "isPublic": true,
      "isDeleted": false,
      "defaultLocation": "Online",
      "maxAttendees": 100,
      "minAttendees": 1,
      "timeScale": 15,
      "createdBy": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
      "created": "2024-05-12T11:30:12.6164258"
    },
    "numOfValidReservations": 2,
    "reservations": [
      {
        "reservation": {
          "id": "b927571f-46be-44be-9071-ef121f8007e8",
          "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
          "calendarId": "7e826091-6147-43e5-aea2-7ee444b56eec",
          "name": "テスト予約",
          "description": "テストで予約してみます。",
          "color": null,
          "cartId": null,
          "startFrom": "2024-05-15T09:00:00",
          "endAt": "2024-05-15T09:15:00",
          "isWholeDay": false,
          "bookerId": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
          "status": "New",
          "underName": "SHO SHIMODA",
          "created": "2024-05-12T21:22:20.0816013",
          "isDeleted": false,
          "deleted": "0001-01-01T00:00:00"
        }
      },
      {
        "reservation": {
          "id": "7771e57d-b826-42fe-9949-57b47db4c5df",
          "organizationId": "e8cc59de-9a30-454e-ab1e-1f3fbbcb81bd",
          "calendarId": "7e826091-6147-43e5-aea2-7ee444b56eec",
          "name": "テスト予約2回目",
          "description": "テストで2回目予約してみます。",
          "color": null,
          "cartId": null,
          "startFrom": "2024-05-16T09:00:00",
          "endAt": "2024-05-16T09:15:00",
          "isWholeDay": false,
          "bookerId": "7ec506db-c6ac-480e-9e65-88478cf4f1e1",
          "status": "New",
          "underName": "SHO SHIMODA",
          "created": "2024-05-12T21:23:46.3229625",
          "isDeleted": false,
          "deleted": "0001-01-01T00:00:00"
        }
      }
    ]
  }
}
```

# まとめ

今日は一旦コアとなる部分を作成しました。ここからvalidationなど細かな処理であったりセキュリティー強化を追加して商品化していきます。

# レシートローラー

レシートローラーは月々550円でLINEミニアプリ会員カード・電子レシート発行など、レジ周りのDXを推進しています。興味のある方は是非ホームページよりお問合せくださいませ。 https://receiptroller.com


本日書いたコードはこちらにありますのでご参照ください。

https://github.com/Receipt-Roller/reservations/tree/main/wppReservations/Areas/Api1