現在レシートローラーでは様々なアドオンサービスを構築しています。まだリリースとはいきませんが、現在検討している予約サービスのAPIを構築してみたいと思います。いろいろお店や教室で利用いただけるように、サービス化していきたいとは思いますが、まずは既存サービスのサポートを受けるための予約サービスを作ってみます。最終的には他サービスにも連携するような予約APIを想定しています。
作るものをざっくり絵にする
先ずは絵にしてみます。この段階では詳細は決まってなくても大丈夫です(持論)。
ざっくりとこんな感じのサービスを作ります。
管理者、サービス、クライアントサーバー、利用者の4人がいる想定です。
- まず、管理者がカレンダーや予約可能な商品を作成します。
- 利用者はカレンダーを選択し、予約可能な日付をクライアントサーバーへリクエストします。
- クライアントサーバーはサービスに同じく対象カレンダーの予約可能な日付をリクエストします。
- サービスからの値を加工して利用者に予約可能な日付を表示します。
- 利用者は予約をしたいスロットを選択し、クライアント経由でサービスに予約をいれます。
- 予約の確認メールまたは何かしらのお知らせを利用者に送ります、同じく管理者にも送ります。
というのが、流れと範囲です。
データーの構成としては、ここもざっくりこんな感じです。
アカウントがあり、組織というグループ要素があり、その中にカレンダーが複数あり、カレンダーの中に予約が複数あるようなイメージです。
ここでいうカレンダーは会議室だったり、飲食店のテーブルだったり、ヨガ教室の部屋だったりになります。各要素ごとに細かな設定項目などはあるかと思いますが、一旦ざっくり構成だけ書いてみます。
データの定義を書く
作りたいモノの絵をかいたところで、次は具体的に書くデータのモデルを書き起こします。今回はAzure SQLを使って進めます。
ユーザーと組織を定義
一番大きな枠で、アカウントと組織がありますので、まずはこの2つのモデルから。アカウントに関してはASP.NET Core Identityを最終的には継承します。 ここでいうユーザーは管理者と予約をする利用者の両方になります。利用者と管理者の区別はroleでつけるものとします。
// 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.
}
カレンダーを定義
次にカレンダー周りを定義していきます。 カレンダーとカレンダーグループ(カレンダーがテーブルや部屋だった場合、カレンダーグループは店舗などを想定)、予約モデルになります。
// 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 も更新します。
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
でデータベースを更新します。
作成されたテーブルを確認して次に進みます。
CRUD
データの定義ができたので、次はデータに対するアクションを書いていきます。C(Create) R(Read (Search + Get)) U(Update) D(Delete) の順で書いていきます.
組織のCRUD
まずは、APIに対するInput/Outputのモデルを定義します。
/// <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 作成
/// <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) 検索
/// <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) 取得
/// <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 更新
/// <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 削除
ここでは物理削除していますが、論理削除でもいいかと思います。
/// <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確認、一旦よさそうなので進めます。
カレンダーのCRUD
次は組織内にあるカレンダーのCRUDを作ります。
同じくまずは、APIに対するInput/Outputのモデルを定義します。
/// <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 新規作成
/// <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) 検索
/// <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を確認していますが、本来現在ログインしているユーザーが組織にいて、ロールを持っているか確認するべきだと思いますが、その辺りは追々追加していこうと思います。
/// <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 更新
/// <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 削除
/// <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確認、一旦よさそうなので進めます。
予約のCRUD
次はカレンダー内にある予約のCRUDを作ります。
同じくまずは、APIに対するInput/Outputのモデルを定義します。
/// <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 作成
/// <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) 検索
/// <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) 取得
/// <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 更新
/// <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 削除
/// <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そうなので、実際にコールしてみます。
カレンダーを作って表示してみる
では、早速作ったAPIを使ってカレンダーを表示してみましょう。
(local環境で動かしています)
ログイン
まずは、login
してtoken
を取得します。
リクエスト
curl -X 'POST' \
'https://localhost:7135/login' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"username": "{username}",
"password": "{password}"
}'
レスポンス
{
"token": "{token}"
}
組織を作成
まだデータベースになにも入っていないので、組織から作ります。
リクエスト
curl -X 'POST' \
'https://localhost:7135/organization' \
-H 'accept: text/plain' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-d '{
"name": "(株)レシートローラー"
}'
レスポンス
{
"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
}'
レスポンス
{
"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
}'
レスポンス
{
"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"
}'
レスポンス
{
"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"
}
}
}
正常に予約ができました。 では、もう1個予約を入れてからカレンダーのデータをリクエストしてみます。
リクエスト
{
"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}'
レスポンス
{
"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
本日書いたコードはこちらにありますのでご参照ください。